From 9a2d99e28a5c2989b2db4023acae4f550885f2ef Mon Sep 17 00:00:00 2001 From: Stefan Krah Date: Sat, 25 Feb 2012 12:24:21 +0100 Subject: [PATCH] - Issue #10181: New memoryview implementation fixes multiple ownership and lifetime issues of dynamically allocated Py_buffer members (#9990) as well as crashes (#8305, #7433). Many new features have been added (See whatsnew/3.3), and the documentation has been updated extensively. The ndarray test object from _testbuffer.c implements all aspects of PEP-3118, so further development towards the complete implementation of the PEP can proceed in a test-driven manner. Thanks to Nick Coghlan, Antoine Pitrou and Pauli Virtanen for review and many ideas. - Issue #12834: Fix incorrect results of memoryview.tobytes() for non-contiguous arrays. - Issue #5231: Introduce memoryview.cast() method that allows changing format and shape without making a copy of the underlying memory. --- Doc/c-api/buffer.rst | 547 +++-- Doc/c-api/memoryview.rst | 29 +- Doc/c-api/typeobj.rst | 96 +- Doc/library/stdtypes.rst | 296 ++- Doc/whatsnew/3.3.rst | 56 + Include/abstract.h | 2 +- Include/memoryobject.h | 84 +- Include/object.h | 7 +- Lib/ctypes/test/test_pep3118.py | 76 +- Lib/test/test_buffer.py | 3437 +++++++++++++++++++++++++++++++ Lib/test/test_memoryview.py | 50 +- Lib/test/test_sys.py | 4 +- Misc/ACKS | 1 + Misc/NEWS | 17 + Misc/valgrind-python.supp | 11 + Modules/_testbuffer.c | 2683 ++++++++++++++++++++++++ Modules/_testcapimodule.c | 91 - Objects/abstract.c | 14 +- Objects/memoryobject.c | 3131 +++++++++++++++++++++------- Objects/object.c | 3 + PCbuild/_testbuffer.vcproj | 521 +++++ PCbuild/pcbuild.sln | 21 + PCbuild/readme.txt | 3 + setup.py | 2 + 24 files changed, 10011 insertions(+), 1171 deletions(-) create mode 100644 Lib/test/test_buffer.py create mode 100644 Modules/_testbuffer.c create mode 100644 PCbuild/_testbuffer.vcproj diff --git a/Doc/c-api/buffer.rst b/Doc/c-api/buffer.rst index d98ece3e98e..2d199922f2e 100644 --- a/Doc/c-api/buffer.rst +++ b/Doc/c-api/buffer.rst @@ -7,6 +7,7 @@ Buffer Protocol .. sectionauthor:: Greg Stein .. sectionauthor:: Benjamin Peterson +.. sectionauthor:: Stefan Krah .. index:: @@ -20,7 +21,7 @@ as image processing or numeric analysis. While each of these types have their own semantics, they share the common characteristic of being backed by a possibly large memory buffer. It is -then desireable, in some situations, to access that buffer directly and +then desirable, in some situations, to access that buffer directly and without intermediate copying. Python provides such a facility at the C level in the form of the *buffer @@ -60,8 +61,10 @@ isn't needed anymore. Failure to do so could lead to various issues such as resource leaks. -The buffer structure -==================== +.. _buffer-structure: + +Buffer structure +================ Buffer structures (or simply "buffers") are useful as a way to expose the binary data from another object to the Python programmer. They can also be @@ -81,93 +84,330 @@ can be created. .. c:type:: Py_buffer - .. c:member:: void *buf + .. c:member:: void \*obj - A pointer to the start of the memory for the object. + A new reference to the exporting object or *NULL*. The reference is owned + by the consumer and automatically decremented and set to *NULL* by + :c:func:`PyBuffer_Release`. + + For temporary buffers that are wrapped by :c:func:`PyMemoryView_FromBuffer` + this field must be *NULL*. + + .. c:member:: void \*buf + + A pointer to the start of the logical structure described by the buffer + fields. This can be any location within the underlying physical memory + block of the exporter. For example, with negative :c:member:`~Py_buffer.strides` + the value may point to the end of the memory block. + + For contiguous arrays, the value points to the beginning of the memory + block. .. c:member:: Py_ssize_t len - :noindex: - The total length of the memory in bytes. + ``product(shape) * itemsize``. For contiguous arrays, this is the length + of the underlying memory block. For non-contiguous arrays, it is the length + that the logical structure would have if it were copied to a contiguous + representation. + + Accessing ``((char *)buf)[0] up to ((char *)buf)[len-1]`` is only valid + if the buffer has been obtained by a request that guarantees contiguity. In + most cases such a request will be :c:macro:`PyBUF_SIMPLE` or :c:macro:`PyBUF_WRITABLE`. .. c:member:: int readonly - An indicator of whether the buffer is read only. - - .. c:member:: const char *format - :noindex: - - A *NULL* terminated string in :mod:`struct` module style syntax giving - the contents of the elements available through the buffer. If this is - *NULL*, ``"B"`` (unsigned bytes) is assumed. - - .. c:member:: int ndim - - The number of dimensions the memory represents as a multi-dimensional - array. If it is 0, :c:data:`strides` and :c:data:`suboffsets` must be - *NULL*. - - .. c:member:: Py_ssize_t *shape - - An array of :c:type:`Py_ssize_t`\s the length of :c:data:`ndim` giving the - shape of the memory as a multi-dimensional array. Note that - ``((*shape)[0] * ... * (*shape)[ndims-1])*itemsize`` should be equal to - :c:data:`len`. - - .. c:member:: Py_ssize_t *strides - - An array of :c:type:`Py_ssize_t`\s the length of :c:data:`ndim` giving the - number of bytes to skip to get to a new element in each dimension. - - .. c:member:: Py_ssize_t *suboffsets - - An array of :c:type:`Py_ssize_t`\s the length of :c:data:`ndim`. If these - suboffset numbers are greater than or equal to 0, then the value stored - along the indicated dimension is a pointer and the suboffset value - dictates how many bytes to add to the pointer after de-referencing. A - suboffset value that it negative indicates that no de-referencing should - occur (striding in a contiguous memory block). - - Here is a function that returns a pointer to the element in an N-D array - pointed to by an N-dimensional index when there are both non-NULL strides - and suboffsets:: - - void *get_item_pointer(int ndim, void *buf, Py_ssize_t *strides, - Py_ssize_t *suboffsets, Py_ssize_t *indices) { - char *pointer = (char*)buf; - int i; - for (i = 0; i < ndim; i++) { - pointer += strides[i] * indices[i]; - if (suboffsets[i] >=0 ) { - pointer = *((char**)pointer) + suboffsets[i]; - } - } - return (void*)pointer; - } - + An indicator of whether the buffer is read-only. This field is controlled + by the :c:macro:`PyBUF_WRITABLE` flag. .. c:member:: Py_ssize_t itemsize - This is a storage for the itemsize (in bytes) of each element of the - shared memory. It is technically un-necessary as it can be obtained - using :c:func:`PyBuffer_SizeFromFormat`, however an exporter may know - this information without parsing the format string and it is necessary - to know the itemsize for proper interpretation of striding. Therefore, - storing it is more convenient and faster. + Item size in bytes of a single element. Same as the value of :func:`struct.calcsize` + called on non-NULL :c:member:`~Py_buffer.format` values. - .. c:member:: void *internal + Important exception: If a consumer requests a buffer without the + :c:macro:`PyBUF_FORMAT` flag, :c:member:`~Py_Buffer.format` will + be set to *NULL*, but :c:member:`~Py_buffer.itemsize` still has + the value for the original format. + + If :c:member:`~Py_Buffer.shape` is present, the equality + ``product(shape) * itemsize == len`` still holds and the consumer + can use :c:member:`~Py_buffer.itemsize` to navigate the buffer. + + If :c:member:`~Py_Buffer.shape` is *NULL* as a result of a :c:macro:`PyBUF_SIMPLE` + or a :c:macro:`PyBUF_WRITABLE` request, the consumer must disregard + :c:member:`~Py_buffer.itemsize` and assume ``itemsize == 1``. + + .. c:member:: const char \*format + + A *NUL* terminated string in :mod:`struct` module style syntax describing + the contents of a single item. If this is *NULL*, ``"B"`` (unsigned bytes) + is assumed. + + This field is controlled by the :c:macro:`PyBUF_FORMAT` flag. + + .. c:member:: int ndim + + The number of dimensions the memory represents as an n-dimensional array. + If it is 0, :c:member:`~Py_Buffer.buf` points to a single item representing + a scalar. In this case, :c:member:`~Py_buffer.shape`, :c:member:`~Py_buffer.strides` + and :c:member:`~Py_buffer.suboffsets` MUST be *NULL*. + + The macro :c:macro:`PyBUF_MAX_NDIM` limits the maximum number of dimensions + to 64. Exporters MUST respect this limit, consumers of multi-dimensional + buffers SHOULD be able to handle up to :c:macro:`PyBUF_MAX_NDIM` dimensions. + + .. c:member:: Py_ssize_t \*shape + + An array of :c:type:`Py_ssize_t` of length :c:member:`~Py_buffer.ndim` + indicating the shape of the memory as an n-dimensional array. Note that + ``shape[0] * ... * shape[ndim-1] * itemsize`` MUST be equal to + :c:member:`~Py_buffer.len`. + + Shape values are restricted to ``shape[n] >= 0``. The case + ``shape[n] == 0`` requires special attention. See `complex arrays`_ + for further information. + + The shape array is read-only for the consumer. + + .. c:member:: Py_ssize_t \*strides + + An array of :c:type:`Py_ssize_t` of length :c:member:`~Py_buffer.ndim` + giving the number of bytes to skip to get to a new element in each + dimension. + + Stride values can be any integer. For regular arrays, strides are + usually positive, but a consumer MUST be able to handle the case + ``strides[n] <= 0``. See `complex arrays`_ for further information. + + The strides array is read-only for the consumer. + + .. c:member:: Py_ssize_t \*suboffsets + + An array of :c:type:`Py_ssize_t` of length :c:member:`~Py_buffer.ndim`. + If ``suboffsets[n] >= 0``, the values stored along the nth dimension are + pointers and the suboffset value dictates how many bytes to add to each + pointer after de-referencing. A suboffset value that is negative + indicates that no de-referencing should occur (striding in a contiguous + memory block). + + This type of array representation is used by the Python Imaging Library + (PIL). See `complex arrays`_ for further information how to access elements + of such an array. + + The suboffsets array is read-only for the consumer. + + .. c:member:: void \*internal This is for use internally by the exporting object. For example, this might be re-cast as an integer by the exporter and used to store flags about whether or not the shape, strides, and suboffsets arrays must be - freed when the buffer is released. The consumer should never alter this + freed when the buffer is released. The consumer MUST NOT alter this value. +.. _buffer-request-types: + +Buffer request types +==================== + +Buffers are usually obtained by sending a buffer request to an exporting +object via :c:func:`PyObject_GetBuffer`. Since the complexity of the logical +structure of the memory can vary drastically, the consumer uses the *flags* +argument to specify the exact buffer type it can handle. + +All :c:data:`Py_buffer` fields are unambiguously defined by the request +type. + +request-independent fields +~~~~~~~~~~~~~~~~~~~~~~~~~~ +The following fields are not influenced by *flags* and must always be filled in +with the correct values: :c:member:`~Py_buffer.obj`, :c:member:`~Py_buffer.buf`, +:c:member:`~Py_buffer.len`, :c:member:`~Py_buffer.itemsize`, :c:member:`~Py_buffer.ndim`. + + +readonly, format +~~~~~~~~~~~~~~~~ + + .. c:macro:: PyBUF_WRITABLE + + Controls the :c:member:`~Py_buffer.readonly` field. If set, the exporter + MUST provide a writable buffer or else report failure. Otherwise, the + exporter MAY provide either a read-only or writable buffer, but the choice + MUST be consistent for all consumers. + + .. c:macro:: PyBUF_FORMAT + + Controls the :c:member:`~Py_buffer.format` field. If set, this field MUST + be filled in correctly. Otherwise, this field MUST be *NULL*. + + +:c:macro:`PyBUF_WRITABLE` can be \|'d to any of the flags in the next section. +Since :c:macro:`PyBUF_SIMPLE` is defined as 0, :c:macro:`PyBUF_WRITABLE` +can be used as a stand-alone flag to request a simple writable buffer. + +:c:macro:`PyBUF_FORMAT` can be \|'d to any of the flags except :c:macro:`PyBUF_SIMPLE`. +The latter already implies format ``B`` (unsigned bytes). + + +shape, strides, suboffsets +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The flags that control the logical structure of the memory are listed +in decreasing order of complexity. Note that each flag contains all bits +of the flags below it. + + ++-----------------------------+-------+---------+------------+ +| Request | shape | strides | suboffsets | ++=============================+=======+=========+============+ +| .. c:macro:: PyBUF_INDIRECT | yes | yes | if needed | ++-----------------------------+-------+---------+------------+ +| .. c:macro:: PyBUF_STRIDES | yes | yes | NULL | ++-----------------------------+-------+---------+------------+ +| .. c:macro:: PyBUF_ND | yes | NULL | NULL | ++-----------------------------+-------+---------+------------+ +| .. c:macro:: PyBUF_SIMPLE | NULL | NULL | NULL | ++-----------------------------+-------+---------+------------+ + + +contiguity requests +~~~~~~~~~~~~~~~~~~~ + +C or Fortran contiguity can be explicitly requested, with and without stride +information. Without stride information, the buffer must be C-contiguous. + ++-----------------------------------+-------+---------+------------+--------+ +| Request | shape | strides | suboffsets | contig | ++===================================+=======+=========+============+========+ +| .. c:macro:: PyBUF_C_CONTIGUOUS | yes | yes | NULL | C | ++-----------------------------------+-------+---------+------------+--------+ +| .. c:macro:: PyBUF_F_CONTIGUOUS | yes | yes | NULL | F | ++-----------------------------------+-------+---------+------------+--------+ +| .. c:macro:: PyBUF_ANY_CONTIGUOUS | yes | yes | NULL | C or F | ++-----------------------------------+-------+---------+------------+--------+ +| .. c:macro:: PyBUF_ND | yes | NULL | NULL | C | ++-----------------------------------+-------+---------+------------+--------+ + + +compound requests +~~~~~~~~~~~~~~~~~ + +All possible requests are fully defined by some combination of the flags in +the previous section. For convenience, the buffer protocol provides frequently +used combinations as single flags. + +In the following table *U* stands for undefined contiguity. The consumer would +have to call :c:func:`PyBuffer_IsContiguous` to determine contiguity. + + + ++-------------------------------+-------+---------+------------+--------+----------+--------+ +| Request | shape | strides | suboffsets | contig | readonly | format | ++===============================+=======+=========+============+========+==========+========+ +| .. c:macro:: PyBUF_FULL | yes | yes | if needed | U | 0 | yes | ++-------------------------------+-------+---------+------------+--------+----------+--------+ +| .. c:macro:: PyBUF_FULL_RO | yes | yes | if needed | U | 1 or 0 | yes | ++-------------------------------+-------+---------+------------+--------+----------+--------+ +| .. c:macro:: PyBUF_RECORDS | yes | yes | NULL | U | 0 | yes | ++-------------------------------+-------+---------+------------+--------+----------+--------+ +| .. c:macro:: PyBUF_RECORDS_RO | yes | yes | NULL | U | 1 or 0 | yes | ++-------------------------------+-------+---------+------------+--------+----------+--------+ +| .. c:macro:: PyBUF_STRIDED | yes | yes | NULL | U | 0 | NULL | ++-------------------------------+-------+---------+------------+--------+----------+--------+ +| .. c:macro:: PyBUF_STRIDED_RO | yes | yes | NULL | U | 1 or 0 | NULL | ++-------------------------------+-------+---------+------------+--------+----------+--------+ +| .. c:macro:: PyBUF_CONTIG | yes | NULL | NULL | C | 0 | NULL | ++-------------------------------+-------+---------+------------+--------+----------+--------+ +| .. c:macro:: PyBUF_CONTIG_RO | yes | NULL | NULL | C | 1 or 0 | NULL | ++-------------------------------+-------+---------+------------+--------+----------+--------+ + + +Complex arrays +============== + +NumPy-style: shape and strides +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The logical structure of NumPy-style arrays is defined by :c:member:`~Py_buffer.itemsize`, +:c:member:`~Py_buffer.ndim`, :c:member:`~Py_buffer.shape` and :c:member:`~Py_buffer.strides`. + +If ``ndim == 0``, the memory location pointed to by :c:member:`~Py_buffer.buf` is +interpreted as a scalar of size :c:member:`~Py_buffer.itemsize`. In that case, +both :c:member:`~Py_buffer.shape` and :c:member:`~Py_buffer.strides` are *NULL*. + +If :c:member:`~Py_buffer.strides` is *NULL*, the array is interpreted as +a standard n-dimensional C-array. Otherwise, the consumer must access an +n-dimensional array as follows: + + ``ptr = (char *)buf + indices[0] * strides[0] + ... + indices[n-1] * strides[n-1]`` + ``item = *((typeof(item) *)ptr);`` + + +As noted above, :c:member:`~Py_buffer.buf` can point to any location within +the actual memory block. An exporter can check the validity of a buffer with +this function: + +.. code-block:: python + + def verify_structure(memlen, itemsize, ndim, shape, strides, offset): + """Verify that the parameters represent a valid array within + the bounds of the allocated memory: + char *mem: start of the physical memory block + memlen: length of the physical memory block + offset: (char *)buf - mem + """ + if offset % itemsize: + return False + if offset < 0 or offset+itemsize > memlen: + return False + if any(v % itemsize for v in strides): + return False + + if ndim <= 0: + return ndim == 0 and not shape and not strides + if 0 in shape: + return True + + imin = sum(strides[j]*(shape[j]-1) for j in range(ndim) + if strides[j] <= 0) + imax = sum(strides[j]*(shape[j]-1) for j in range(ndim) + if strides[j] > 0) + + return 0 <= offset+imin and offset+imax+itemsize <= memlen + + +PIL-style: shape, strides and suboffsets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to the regular items, PIL-style arrays can contain pointers +that must be followed in order to get to the next element in a dimension. +For example, the regular three-dimensional C-array ``char v[2][2][3]`` can +also be viewed as an array of 2 pointers to 2 two-dimensional arrays: +``char (*v[2])[2][3]``. In suboffsets representation, those two pointers +can be embedded at the start of :c:member:`~Py_buffer.buf`, pointing +to two ``char x[2][3]`` arrays that can be located anywhere in memory. + + +Here is a function that returns a pointer to the element in an N-D array +pointed to by an N-dimensional index when there are both non-NULL strides +and suboffsets:: + + void *get_item_pointer(int ndim, void *buf, Py_ssize_t *strides, + Py_ssize_t *suboffsets, Py_ssize_t *indices) { + char *pointer = (char*)buf; + int i; + for (i = 0; i < ndim; i++) { + pointer += strides[i] * indices[i]; + if (suboffsets[i] >=0 ) { + pointer = *((char**)pointer) + suboffsets[i]; + } + } + return (void*)pointer; + } + Buffer-related functions ======================== - .. c:function:: int PyObject_CheckBuffer(PyObject *obj) Return 1 if *obj* supports the buffer interface otherwise 0. When 1 is @@ -175,152 +415,69 @@ Buffer-related functions succeed. -.. c:function:: int PyObject_GetBuffer(PyObject *obj, Py_buffer *view, int flags) +.. c:function:: int PyObject_GetBuffer(PyObject *exporter, Py_buffer *view, int flags) - Export a view over some internal data from the target object *obj*. - *obj* must not be NULL, and *view* must point to an existing - :c:type:`Py_buffer` structure allocated by the caller (most uses of - this function will simply declare a local variable of type - :c:type:`Py_buffer`). The *flags* argument is a bit field indicating - what kind of buffer is requested. The buffer interface allows - for complicated memory layout possibilities; however, some callers - won't want to handle all the complexity and instead request a simple - view of the target object (using :c:macro:`PyBUF_SIMPLE` for a read-only - view and :c:macro:`PyBUF_WRITABLE` for a read-write view). + Send a request to *exporter* to fill in *view* as specified by *flags*. + If the exporter cannot provide a buffer of the exact type, it MUST raise + :c:data:`PyExc_BufferError`, set :c:member:`view->obj` to *NULL* and + return -1. - Some exporters may not be able to share memory in every possible way and - may need to raise errors to signal to some consumers that something is - just not possible. These errors should be a :exc:`BufferError` unless - there is another error that is actually causing the problem. The - exporter can use flags information to simplify how much of the - :c:data:`Py_buffer` structure is filled in with non-default values and/or - raise an error if the object can't support a simpler view of its memory. + On success, fill in *view*, set :c:member:`view->obj` to a new reference + to *exporter* and return 0. - On success, 0 is returned and the *view* structure is filled with useful - values. On error, -1 is returned and an exception is raised; the *view* - is left in an undefined state. - - The following are the possible values to the *flags* arguments. - - .. c:macro:: PyBUF_SIMPLE - - This is the default flag. The returned buffer exposes a read-only - memory area. The format of data is assumed to be raw unsigned bytes, - without any particular structure. This is a "stand-alone" flag - constant. It never needs to be '|'d to the others. The exporter will - raise an error if it cannot provide such a contiguous buffer of bytes. - - .. c:macro:: PyBUF_WRITABLE - - Like :c:macro:`PyBUF_SIMPLE`, but the returned buffer is writable. If - the exporter doesn't support writable buffers, an error is raised. - - .. c:macro:: PyBUF_STRIDES - - This implies :c:macro:`PyBUF_ND`. The returned buffer must provide - strides information (i.e. the strides cannot be NULL). This would be - used when the consumer can handle strided, discontiguous arrays. - Handling strides automatically assumes you can handle shape. The - exporter can raise an error if a strided representation of the data is - not possible (i.e. without the suboffsets). - - .. c:macro:: PyBUF_ND - - The returned buffer must provide shape information. The memory will be - assumed C-style contiguous (last dimension varies the fastest). The - exporter may raise an error if it cannot provide this kind of - contiguous buffer. If this is not given then shape will be *NULL*. - - .. c:macro:: PyBUF_C_CONTIGUOUS - PyBUF_F_CONTIGUOUS - PyBUF_ANY_CONTIGUOUS - - These flags indicate that the contiguity returned buffer must be - respectively, C-contiguous (last dimension varies the fastest), Fortran - contiguous (first dimension varies the fastest) or either one. All of - these flags imply :c:macro:`PyBUF_STRIDES` and guarantee that the - strides buffer info structure will be filled in correctly. - - .. c:macro:: PyBUF_INDIRECT - - This flag indicates the returned buffer must have suboffsets - information (which can be NULL if no suboffsets are needed). This can - be used when the consumer can handle indirect array referencing implied - by these suboffsets. This implies :c:macro:`PyBUF_STRIDES`. - - .. c:macro:: PyBUF_FORMAT - - The returned buffer must have true format information if this flag is - provided. This would be used when the consumer is going to be checking - for what 'kind' of data is actually stored. An exporter should always - be able to provide this information if requested. If format is not - explicitly requested then the format must be returned as *NULL* (which - means ``'B'``, or unsigned bytes). - - .. c:macro:: PyBUF_STRIDED - - This is equivalent to ``(PyBUF_STRIDES | PyBUF_WRITABLE)``. - - .. c:macro:: PyBUF_STRIDED_RO - - This is equivalent to ``(PyBUF_STRIDES)``. - - .. c:macro:: PyBUF_RECORDS - - This is equivalent to ``(PyBUF_STRIDES | PyBUF_FORMAT | - PyBUF_WRITABLE)``. - - .. c:macro:: PyBUF_RECORDS_RO - - This is equivalent to ``(PyBUF_STRIDES | PyBUF_FORMAT)``. - - .. c:macro:: PyBUF_FULL - - This is equivalent to ``(PyBUF_INDIRECT | PyBUF_FORMAT | - PyBUF_WRITABLE)``. - - .. c:macro:: PyBUF_FULL_RO - - This is equivalent to ``(PyBUF_INDIRECT | PyBUF_FORMAT)``. - - .. c:macro:: PyBUF_CONTIG - - This is equivalent to ``(PyBUF_ND | PyBUF_WRITABLE)``. - - .. c:macro:: PyBUF_CONTIG_RO - - This is equivalent to ``(PyBUF_ND)``. + Successful calls to :c:func:`PyObject_GetBuffer` must be paired with calls + to :c:func:`PyBuffer_Release`, similar to :c:func:`malloc` and :c:func:`free`. + Thus, after the consumer is done with the buffer, :c:func:`PyBuffer_Release` + must be called exactly once. .. c:function:: void PyBuffer_Release(Py_buffer *view) - Release the buffer *view*. This should be called when the buffer is no - longer being used as it may free memory from it. + Release the buffer *view* and decrement the reference count for + :c:member:`view->obj`. This function MUST be called when the buffer + is no longer being used, otherwise reference leaks may occur. + + It is an error to call this function on a buffer that was not obtained via + :c:func:`PyObject_GetBuffer`. .. c:function:: Py_ssize_t PyBuffer_SizeFromFormat(const char *) - Return the implied :c:data:`~Py_buffer.itemsize` from the struct-stype - :c:data:`~Py_buffer.format`. + Return the implied :c:data:`~Py_buffer.itemsize` from :c:data:`~Py_buffer.format`. + This function is not yet implemented. -.. c:function:: int PyBuffer_IsContiguous(Py_buffer *view, char fortran) +.. c:function:: int PyBuffer_IsContiguous(Py_buffer *view, char order) - Return 1 if the memory defined by the *view* is C-style (*fortran* is - ``'C'``) or Fortran-style (*fortran* is ``'F'``) contiguous or either one - (*fortran* is ``'A'``). Return 0 otherwise. + Return 1 if the memory defined by the *view* is C-style (*order* is + ``'C'``) or Fortran-style (*order* is ``'F'``) contiguous or either one + (*order* is ``'A'``). Return 0 otherwise. -.. c:function:: void PyBuffer_FillContiguousStrides(int ndim, Py_ssize_t *shape, Py_ssize_t *strides, Py_ssize_t itemsize, char fortran) +.. c:function:: void PyBuffer_FillContiguousStrides(int ndim, Py_ssize_t *shape, Py_ssize_t *strides, Py_ssize_t itemsize, char order) Fill the *strides* array with byte-strides of a contiguous (C-style if - *fortran* is ``'C'`` or Fortran-style if *fortran* is ``'F'``) array of the + *order* is ``'C'`` or Fortran-style if *order* is ``'F'``) array of the given shape with the given number of bytes per element. -.. c:function:: int PyBuffer_FillInfo(Py_buffer *view, PyObject *obj, void *buf, Py_ssize_t len, int readonly, int infoflags) +.. c:function:: int PyBuffer_FillInfo(Py_buffer *view, PyObject *exporter, void *buf, Py_ssize_t len, int readonly, int flags) + + Handle buffer requests for an exporter that wants to expose *buf* of size *len* + with writability set according to *readonly*. *buf* is interpreted as a sequence + of unsigned bytes. + + The *flags* argument indicates the request type. This function always fills in + *view* as specified by flags, unless *buf* has been designated as read-only + and :c:macro:`PyBUF_WRITABLE` is set in *flags*. + + On success, set :c:member:`view->obj` to a new reference to *exporter* and + return 0. Otherwise, raise :c:data:`PyExc_BufferError`, set + :c:member:`view->obj` to *NULL* and return -1; + + If this function is used as part of a :ref:`getbufferproc `, + *exporter* MUST be set to the exporting object. Otherwise, *exporter* MUST + be NULL. + - Fill in a buffer-info structure, *view*, correctly for an exporter that can - only share a contiguous chunk of memory of "unsigned bytes" of the given - length. Return 0 on success and -1 (with raising an error) on error. diff --git a/Doc/c-api/memoryview.rst b/Doc/c-api/memoryview.rst index 6b49cdf70a3..ef039753afc 100644 --- a/Doc/c-api/memoryview.rst +++ b/Doc/c-api/memoryview.rst @@ -17,16 +17,19 @@ any other object. Create a memoryview object from an object that provides the buffer interface. If *obj* supports writable buffer exports, the memoryview object will be - readable and writable, otherwise it will be read-only. + read/write, otherwise it may be either read-only or read/write at the + discretion of the exporter. +.. c:function:: PyObject *PyMemoryView_FromMemory(char *mem, Py_ssize_t size, int flags) + + Create a memoryview object using *mem* as the underlying buffer. + *flags* can be one of :c:macro:`PyBUF_READ` or :c:macro:`PyBUF_WRITE`. .. c:function:: PyObject *PyMemoryView_FromBuffer(Py_buffer *view) Create a memoryview object wrapping the given buffer structure *view*. - The memoryview object then owns the buffer represented by *view*, which - means you shouldn't try to call :c:func:`PyBuffer_Release` yourself: it - will be done on deallocation of the memoryview object. - + For simple byte buffers, :c:func:`PyMemoryView_FromMemory` is the preferred + function. .. c:function:: PyObject *PyMemoryView_GetContiguous(PyObject *obj, int buffertype, char order) @@ -43,10 +46,16 @@ any other object. currently allowed to create subclasses of :class:`memoryview`. -.. c:function:: Py_buffer *PyMemoryView_GET_BUFFER(PyObject *obj) +.. c:function:: Py_buffer *PyMemoryView_GET_BUFFER(PyObject *mview) - Return a pointer to the buffer structure wrapped by the given - memoryview object. The object **must** be a memoryview instance; - this macro doesn't check its type, you must do it yourself or you - will risk crashes. + Return a pointer to the memoryview's private copy of the exporter's buffer. + *mview* **must** be a memoryview instance; this macro doesn't check its type, + you must do it yourself or you will risk crashes. + +.. c:function:: Py_buffer *PyMemoryView_GET_BASE(PyObject *mview) + + Return either a pointer to the exporting object that the memoryview is based + on or *NULL* if the memoryview has been created by one of the functions + :c:func:`PyMemoryView_FromMemory` or :c:func:`PyMemoryView_FromBuffer`. + *mview* **must** be a memoryview instance. diff --git a/Doc/c-api/typeobj.rst b/Doc/c-api/typeobj.rst index 68ca9adaa9b..b15d927719f 100644 --- a/Doc/c-api/typeobj.rst +++ b/Doc/c-api/typeobj.rst @@ -1198,46 +1198,74 @@ Buffer Object Structures .. sectionauthor:: Greg J. Stein .. sectionauthor:: Benjamin Peterson - - -The :ref:`buffer interface ` exports a model where an object can expose its internal -data. - -If an object does not export the buffer interface, then its :attr:`tp_as_buffer` -member in the :c:type:`PyTypeObject` structure should be *NULL*. Otherwise, the -:attr:`tp_as_buffer` will point to a :c:type:`PyBufferProcs` structure. - +.. sectionauthor:: Stefan Krah .. c:type:: PyBufferProcs - Structure used to hold the function pointers which define an implementation of - the buffer protocol. + This structure holds pointers to the functions required by the + :ref:`Buffer protocol `. The protocol defines how + an exporter object can expose its internal data to consumer objects. - .. c:member:: getbufferproc bf_getbuffer +.. c:member:: getbufferproc PyBufferProcs.bf_getbuffer - This should fill a :c:type:`Py_buffer` with the necessary data for - exporting the type. The signature of :data:`getbufferproc` is ``int - (PyObject *obj, Py_buffer *view, int flags)``. *obj* is the object to - export, *view* is the :c:type:`Py_buffer` struct to fill, and *flags* gives - the conditions the caller wants the memory under. (See - :c:func:`PyObject_GetBuffer` for all flags.) :c:member:`bf_getbuffer` is - responsible for filling *view* with the appropriate information. - (:c:func:`PyBuffer_FillView` can be used in simple cases.) See - :c:type:`Py_buffer`\s docs for what needs to be filled in. + The signature of this function is:: + + int (PyObject *exporter, Py_buffer *view, int flags); + + Handle a request to *exporter* to fill in *view* as specified by *flags*. + A standard implementation of this function will take these steps: + + - Check if the request can be met. If not, raise :c:data:`PyExc_BufferError`, + set :c:data:`view->obj` to *NULL* and return -1. + + - Fill in the requested fields. + + - Increment an internal counter for the number of exports. + + - Set :c:data:`view->obj` to *exporter* and increment :c:data:`view->obj`. + + - Return 0. + + The individual fields of *view* are described in section + :ref:`Buffer structure `, the rules how an exporter + must react to specific requests are in section + :ref:`Buffer request types `. + + All memory pointed to in the :c:type:`Py_buffer` structure belongs to + the exporter and must remain valid until there are no consumers left. + :c:member:`~Py_buffer.shape`, :c:member:`~Py_buffer.strides`, + :c:member:`~Py_buffer.suboffsets` and :c:member:`~Py_buffer.internal` + are read-only for the consumer. + + :c:func:`PyBuffer_FillInfo` provides an easy way of exposing a simple + bytes buffer while dealing correctly with all request types. + + :c:func:`PyObject_GetBuffer` is the interface for the consumer that + wraps this function. + +.. c:member:: releasebufferproc PyBufferProcs.bf_releasebuffer + + The signature of this function is:: + + void (PyObject *exporter, Py_buffer *view); + + Handle a request to release the resources of the buffer. If no resources + need to be released, this field may be *NULL*. A standard implementation + of this function will take these steps: + + - Decrement an internal counter for the number of exports. + + - If the counter is 0, free all memory associated with *view*. + + The exporter MUST use the :c:member:`~Py_buffer.internal` field to keep + track of buffer-specific resources (if present). This field is guaranteed + to remain constant, while a consumer MAY pass a copy of the original buffer + as the *view* argument. - .. c:member:: releasebufferproc bf_releasebuffer + This function MUST NOT decrement :c:data:`view->obj`, since that is + done automatically in :c:func:`PyBuffer_Release`. - This should release the resources of the buffer. The signature of - :c:data:`releasebufferproc` is ``void (PyObject *obj, Py_buffer *view)``. - If the :c:data:`bf_releasebuffer` function is not provided (i.e. it is - *NULL*), then it does not ever need to be called. - The exporter of the buffer interface must make sure that any memory - pointed to in the :c:type:`Py_buffer` structure remains valid until - releasebuffer is called. Exporters will need to define a - :c:data:`bf_releasebuffer` function if they can re-allocate their memory, - strides, shape, suboffsets, or format variables which they might share - through the struct bufferinfo. - - See :c:func:`PyBuffer_Release`. + :c:func:`PyBuffer_Release` is the interface for the consumer that + wraps this function. diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index a07be4f91f5..183b2f78fc1 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -2377,7 +2377,7 @@ memoryview type :class:`memoryview` objects allow Python code to access the internal data of an object that supports the :ref:`buffer protocol ` without -copying. Memory is generally interpreted as simple bytes. +copying. .. class:: memoryview(obj) @@ -2391,52 +2391,88 @@ copying. Memory is generally interpreted as simple bytes. is a single byte, but other types such as :class:`array.array` may have bigger elements. - ``len(view)`` returns the total number of elements in the memoryview, - *view*. The :class:`~memoryview.itemsize` attribute will give you the + ``len(view)`` is equal to the length of :class:`~memoryview.tolist`. + If ``view.ndim = 0``, the length is 1. If ``view.ndim = 1``, the length + is equal to the number of elements in the view. For higher dimensions, + the length is equal to the length of the nested list representation of + the view. The :class:`~memoryview.itemsize` attribute will give you the number of bytes in a single element. - A :class:`memoryview` supports slicing to expose its data. Taking a single - index will return a single element as a :class:`bytes` object. Full - slicing will result in a subview:: + A :class:`memoryview` supports slicing to expose its data. If + :class:`~memoryview.format` is one of the native format specifiers + from the :mod:`struct` module, indexing will return a single element + with the correct type. Full slicing will result in a subview:: - >>> v = memoryview(b'abcefg') - >>> v[1] - b'b' - >>> v[-1] - b'g' - >>> v[1:4] - - >>> bytes(v[1:4]) - b'bce' + >>> v = memoryview(b'abcefg') + >>> v[1] + 98 + >>> v[-1] + 103 + >>> v[1:4] + + >>> bytes(v[1:4]) + b'bce' - If the object the memoryview is over supports changing its data, the - memoryview supports slice assignment:: + Other native formats:: + + >>> import array + >>> a = array.array('l', [-11111111, 22222222, -33333333, 44444444]) + >>> a[0] + -11111111 + >>> a[-1] + 44444444 + >>> a[2:3].tolist() + [-33333333] + >>> a[::2].tolist() + [-11111111, -33333333] + >>> a[::-1].tolist() + [44444444, -33333333, 22222222, -11111111] + + .. versionadded:: 3.3 + + If the underlying object is writable, the memoryview supports slice + assignment. Resizing is not allowed:: >>> data = bytearray(b'abcefg') >>> v = memoryview(data) >>> v.readonly False - >>> v[0] = b'z' + >>> v[0] = ord(b'z') >>> data bytearray(b'zbcefg') >>> v[1:4] = b'123' >>> data bytearray(b'z123fg') - >>> v[2] = b'spam' + >>> v[2:3] = b'spam' Traceback (most recent call last): - File "", line 1, in - ValueError: cannot modify size of memoryview object + File "", line 1, in + ValueError: memoryview assignment: lvalue and rvalue have different structures + >>> v[2:6] = b'spam' + >>> data + bytearray(b'z1spam') - Notice how the size of the memoryview object cannot be changed. - - Memoryviews of hashable (read-only) types are also hashable and their - hash value matches the corresponding bytes object:: + Memoryviews of hashable (read-only) types are also hashable. The hash + is defined as ``hash(m) == hash(m.tobytes())``:: >>> v = memoryview(b'abcefg') >>> hash(v) == hash(b'abcefg') True >>> hash(v[2:4]) == hash(b'ce') True + >>> hash(v[::-2]) == hash(b'abcefg'[::-2]) + True + + Hashing of multi-dimensional objects is supported:: + + >>> buf = bytes(list(range(12))) + >>> x = memoryview(buf) + >>> y = x.cast('B', shape=[2,2,3]) + >>> x.tolist() + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + >>> y.tolist() + [[[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [9, 10, 11]]] + >>> hash(x) == hash(y) == hash(y.tobytes()) + True .. versionchanged:: 3.3 Memoryview objects are now hashable. @@ -2455,12 +2491,20 @@ copying. Memory is generally interpreted as simple bytes. >>> bytes(m) b'abc' + For non-contiguous arrays the result is equal to the flattened list + representation with all elements converted to bytes. + .. method:: tolist() - Return the data in the buffer as a list of integers. :: + Return the data in the buffer as a list of elements. :: >>> memoryview(b'abc').tolist() [97, 98, 99] + >>> import array + >>> a = array.array('d', [1.1, 2.2, 3.3]) + >>> m = memoryview(a) + >>> m.tolist() + [1.1, 2.2, 3.3] .. method:: release() @@ -2487,7 +2531,7 @@ copying. Memory is generally interpreted as simple bytes. >>> with memoryview(b'abc') as m: ... m[0] ... - b'a' + 97 >>> m[0] Traceback (most recent call last): File "", line 1, in @@ -2495,45 +2539,219 @@ copying. Memory is generally interpreted as simple bytes. .. versionadded:: 3.2 + .. method:: cast(format[, shape]) + + Cast a memoryview to a new format or shape. *shape* defaults to + ``[byte_length//new_itemsize]``, which means that the result view + will be one-dimensional. The return value is a new memoryview, but + the buffer itself is not copied. Supported casts are 1D -> C-contiguous + and C-contiguous -> 1D. One of the formats must be a byte format + ('B', 'b' or 'c'). The byte length of the result must be the same + as the original length. + + Cast 1D/long to 1D/unsigned bytes:: + + >>> import array + >>> a = array.array('l', [1,2,3]) + >>> x = memoryview(a) + >>> x.format + 'l' + >>> x.itemsize + 8 + >>> len(x) + 3 + >>> x.nbytes + 24 + >>> y = x.cast('B') + >>> y.format + 'B' + >>> y.itemsize + 1 + >>> len(y) + 24 + >>> y.nbytes + 24 + + Cast 1D/unsigned bytes to 1D/char:: + + >>> b = bytearray(b'zyz') + >>> x = memoryview(b) + >>> x[0] = b'a' + Traceback (most recent call last): + File "", line 1, in + ValueError: memoryview: invalid value for format "B" + >>> y = x.cast('c') + >>> y[0] = b'a' + >>> b + bytearray(b'ayz') + + Cast 1D/bytes to 3D/ints to 1D/signed char:: + + >>> import struct + >>> buf = struct.pack("i"*12, *list(range(12))) + >>> x = memoryview(buf) + >>> y = x.cast('i', shape=[2,2,3]) + >>> y.tolist() + [[[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [9, 10, 11]]] + >>> y.format + 'i' + >>> y.itemsize + 4 + >>> len(y) + 2 + >>> y.nbytes + 48 + >>> z = y.cast('b') + >>> z.format + 'b' + >>> z.itemsize + 1 + >>> len(z) + 48 + >>> z.nbytes + 48 + + Cast 1D/unsigned char to to 2D/unsigned long:: + + >>> buf = struct.pack("L"*6, *list(range(6))) + >>> x = memoryview(buf) + >>> y = x.cast('L', shape=[2,3]) + >>> len(y) + 2 + >>> y.nbytes + 48 + >>> y.tolist() + [[0, 1, 2], [3, 4, 5]] + + .. versionadded:: 3.3 + There are also several readonly attributes available: + .. attribute:: obj + + The underlying object of the memoryview:: + + >>> b = bytearray(b'xyz') + >>> m = memoryview(b) + >>> m.obj is b + True + + .. versionadded:: 3.3 + + .. attribute:: nbytes + + ``nbytes == product(shape) * itemsize == len(m.tobytes())``. This is + the amount of space in bytes that the array would use in a contiguous + representation. It is not necessarily equal to len(m):: + + >>> import array + >>> a = array.array('i', [1,2,3,4,5]) + >>> m = memoryview(a) + >>> len(m) + 5 + >>> m.nbytes + 20 + >>> y = m[::2] + >>> len(y) + 3 + >>> y.nbytes + 12 + >>> len(y.tobytes()) + 12 + + Multi-dimensional arrays:: + + >>> import struct + >>> buf = struct.pack("d"*12, *[1.5*x for x in range(12)]) + >>> x = memoryview(buf) + >>> y = x.cast('d', shape=[3,4]) + >>> y.tolist() + [[0.0, 1.5, 3.0, 4.5], [6.0, 7.5, 9.0, 10.5], [12.0, 13.5, 15.0, 16.5]] + >>> len(y) + 3 + >>> y.nbytes + 96 + + .. versionadded:: 3.3 + + .. attribute:: readonly + + A bool indicating whether the memory is read only. + .. attribute:: format A string containing the format (in :mod:`struct` module style) for each - element in the view. This defaults to ``'B'``, a simple bytestring. + element in the view. A memoryview can be created from exporters with + arbitrary format strings, but some methods (e.g. :meth:`tolist`) are + restricted to native single element formats. Special care must be taken + when comparing memoryviews. Since comparisons are required to return a + value for ``==`` and ``!=``, two memoryviews referencing the same + exporter can compare as not-equal if the exporter's format is not + understood:: + + >>> from ctypes import BigEndianStructure, c_long + >>> class BEPoint(BigEndianStructure): + ... _fields_ = [("x", c_long), ("y", c_long)] + ... + >>> point = BEPoint(100, 200) + >>> a = memoryview(point) + >>> b = memoryview(point) + >>> a == b + False + >>> a.tolist() + Traceback (most recent call last): + File "", line 1, in + NotImplementedError: memoryview: unsupported format T{>l:x:>l:y:} .. attribute:: itemsize The size in bytes of each element of the memoryview:: - >>> m = memoryview(array.array('H', [1,2,3])) + >>> import array, struct + >>> m = memoryview(array.array('H', [32000, 32001, 32002])) >>> m.itemsize 2 >>> m[0] - b'\x01\x00' - >>> len(m[0]) == m.itemsize + 32000 + >>> struct.calcsize('H') == m.itemsize True - .. attribute:: shape - - A tuple of integers the length of :attr:`ndim` giving the shape of the - memory as a N-dimensional array. - .. attribute:: ndim An integer indicating how many dimensions of a multi-dimensional array the memory represents. + .. attribute:: shape + + A tuple of integers the length of :attr:`ndim` giving the shape of the + memory as a N-dimensional array. + .. attribute:: strides A tuple of integers the length of :attr:`ndim` giving the size in bytes to access each element for each dimension of the array. - .. attribute:: readonly + .. attribute:: suboffsets - A bool indicating whether the memory is read only. + Used internally for PIL-style arrays. The value is informational only. - .. memoryview.suboffsets isn't documented because it only seems useful for C + .. attribute:: c_contiguous + + A bool indicating whether the memory is C-contiguous. + + .. versionadded:: 3.3 + + .. attribute:: f_contiguous + + A bool indicating whether the memory is Fortran contiguous. + + .. versionadded:: 3.3 + + .. attribute:: contiguous + + A bool indicating whether the memory is contiguous. + + .. versionadded:: 3.3 .. _typecontextmanager: diff --git a/Doc/whatsnew/3.3.rst b/Doc/whatsnew/3.3.rst index 20e2914bc08..560331f39b9 100644 --- a/Doc/whatsnew/3.3.rst +++ b/Doc/whatsnew/3.3.rst @@ -49,6 +49,62 @@ This article explains the new features in Python 3.3, compared to 3.2. +.. _pep-3118: + +PEP 3118: New memoryview implementation and buffer protocol documentation +========================================================================= + +:issue:`10181` - memoryview bug fixes and features. + Written by Stefan Krah. + +The new memoryview implementation comprehensively fixes all ownership and +lifetime issues of dynamically allocated fields in the Py_buffer struct +that led to multiple crash reports. Additionally, several functions that +crashed or returned incorrect results for non-contiguous or multi-dimensional +input have been fixed. + +The memoryview object now has a PEP-3118 compliant getbufferproc() +that checks the consumer's request type. Many new features have been +added, most of them work in full generality for non-contiguous arrays +and arrays with suboffsets. + +The documentation has been updated, clearly spelling out responsibilities +for both exporters and consumers. Buffer request flags are grouped into +basic and compound flags. The memory layout of non-contiguous and +multi-dimensional NumPy-style arrays is explained. + +Features +-------- + +* All native single character format specifiers in struct module syntax + (optionally prefixed with '@') are now supported. + +* With some restrictions, the cast() method allows changing of format and + shape of C-contiguous arrays. + +* Multi-dimensional list representations are supported for any array type. + +* Multi-dimensional comparisons are supported for any array type. + +* All array types are hashable if the exporting object is hashable + and the view is read-only. + +* Arbitrary slicing of any 1-D arrays type is supported. For example, it + is now possible to reverse a memoryview in O(1) by using a negative step. + +API changes +----------- + +* The maximum number of dimensions is officially limited to 64. + +* The representation of empty shape, strides and suboffsets is now + an empty tuple instead of None. + +* Accessing a memoryview element with format 'B' (unsigned bytes) + now returns an integer (in accordance with the struct module syntax). + For returning a bytes object the view must be cast to 'c' first. + + .. _pep-393: PEP 393: Flexible String Representation diff --git a/Include/abstract.h b/Include/abstract.h index 3946ec5b388..abb996ff1a4 100644 --- a/Include/abstract.h +++ b/Include/abstract.h @@ -559,7 +559,7 @@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx*/ /* Copy the data from the src buffer to the buffer of destination */ - PyAPI_FUNC(int) PyBuffer_IsContiguous(Py_buffer *view, char fort); + PyAPI_FUNC(int) PyBuffer_IsContiguous(const Py_buffer *view, char fort); PyAPI_FUNC(void) PyBuffer_FillContiguousStrides(int ndims, diff --git a/Include/memoryobject.h b/Include/memoryobject.h index aff5d997f21..4ac6f6525c9 100644 --- a/Include/memoryobject.h +++ b/Include/memoryobject.h @@ -6,70 +6,64 @@ extern "C" { #endif +#ifndef Py_LIMITED_API +PyAPI_DATA(PyTypeObject) _PyManagedBuffer_Type; +#endif PyAPI_DATA(PyTypeObject) PyMemoryView_Type; #define PyMemoryView_Check(op) (Py_TYPE(op) == &PyMemoryView_Type) #ifndef Py_LIMITED_API -/* Get a pointer to the underlying Py_buffer of a memoryview object. */ +/* Get a pointer to the memoryview's private copy of the exporter's buffer. */ #define PyMemoryView_GET_BUFFER(op) (&((PyMemoryViewObject *)(op))->view) -/* Get a pointer to the PyObject from which originates a memoryview object. */ +/* Get a pointer to the exporting object (this may be NULL!). */ #define PyMemoryView_GET_BASE(op) (((PyMemoryViewObject *)(op))->view.obj) #endif - -PyAPI_FUNC(PyObject *) PyMemoryView_GetContiguous(PyObject *base, - int buffertype, - char fort); - - /* Return a contiguous chunk of memory representing the buffer - from an object in a memory view object. If a copy is made then the - base object for the memory view will be a *new* bytes object. - - Otherwise, the base-object will be the object itself and no - data-copying will be done. - - The buffertype argument can be PyBUF_READ, PyBUF_WRITE, - PyBUF_SHADOW to determine whether the returned buffer - should be READONLY, WRITABLE, or set to update the - original buffer if a copy must be made. If buffertype is - PyBUF_WRITE and the buffer is not contiguous an error will - be raised. In this circumstance, the user can use - PyBUF_SHADOW to ensure that a a writable temporary - contiguous buffer is returned. The contents of this - contiguous buffer will be copied back into the original - object after the memoryview object is deleted as long as - the original object is writable and allows setting an - exclusive write lock. If this is not allowed by the - original object, then a BufferError is raised. - - If the object is multi-dimensional and if fortran is 'F', - the first dimension of the underlying array will vary the - fastest in the buffer. If fortran is 'C', then the last - dimension will vary the fastest (C-style contiguous). If - fortran is 'A', then it does not matter and you will get - whatever the object decides is more efficient. - - A new reference is returned that must be DECREF'd when finished. - */ - PyAPI_FUNC(PyObject *) PyMemoryView_FromObject(PyObject *base); - +PyAPI_FUNC(PyObject *) PyMemoryView_FromMemory(char *mem, Py_ssize_t size, + int flags); #ifndef Py_LIMITED_API PyAPI_FUNC(PyObject *) PyMemoryView_FromBuffer(Py_buffer *info); - /* create new if bufptr is NULL - will be a new bytesobject in base */ #endif +PyAPI_FUNC(PyObject *) PyMemoryView_GetContiguous(PyObject *base, + int buffertype, + char order); -/* The struct is declared here so that macros can work, but it shouldn't - be considered public. Don't access those fields directly, use the macros +/* The structs are declared here so that macros can work, but they shouldn't + be considered public. Don't access their fields directly, use the macros and functions instead! */ #ifndef Py_LIMITED_API +#define _Py_MANAGED_BUFFER_RELEASED 0x001 /* access to exporter blocked */ +#define _Py_MANAGED_BUFFER_FREE_FORMAT 0x002 /* free format */ typedef struct { PyObject_HEAD - Py_buffer view; - Py_hash_t hash; + int flags; /* state flags */ + Py_ssize_t exports; /* number of direct memoryview exports */ + Py_buffer master; /* snapshot buffer obtained from the original exporter */ +} _PyManagedBufferObject; + + +/* static storage used for casting between formats */ +#define _Py_MEMORYVIEW_MAX_FORMAT 3 /* must be >= 3 */ + +/* memoryview state flags */ +#define _Py_MEMORYVIEW_RELEASED 0x001 /* access to master buffer blocked */ +#define _Py_MEMORYVIEW_C 0x002 /* C-contiguous layout */ +#define _Py_MEMORYVIEW_FORTRAN 0x004 /* Fortran contiguous layout */ +#define _Py_MEMORYVIEW_SCALAR 0x008 /* scalar: ndim = 0 */ +#define _Py_MEMORYVIEW_PIL 0x010 /* PIL-style layout */ + +typedef struct { + PyObject_VAR_HEAD + _PyManagedBufferObject *mbuf; /* managed buffer */ + Py_hash_t hash; /* hash value for read-only views */ + int flags; /* state flags */ + Py_ssize_t exports; /* number of buffer re-exports */ + Py_buffer view; /* private copy of the exporter's view */ + char format[_Py_MEMORYVIEW_MAX_FORMAT]; /* used for casting */ + Py_ssize_t ob_array[1]; /* shape, strides, suboffsets */ } PyMemoryViewObject; #endif diff --git a/Include/object.h b/Include/object.h index 71d9dc8f762..9b3055df490 100644 --- a/Include/object.h +++ b/Include/object.h @@ -186,15 +186,16 @@ typedef struct bufferinfo { Py_ssize_t *shape; Py_ssize_t *strides; Py_ssize_t *suboffsets; - Py_ssize_t smalltable[2]; /* static store for shape and strides of - mono-dimensional buffers. */ void *internal; } Py_buffer; typedef int (*getbufferproc)(PyObject *, Py_buffer *, int); typedef void (*releasebufferproc)(PyObject *, Py_buffer *); - /* Flags for getting buffers */ +/* Maximum number of dimensions */ +#define PyBUF_MAX_NDIM 64 + +/* Flags for getting buffers */ #define PyBUF_SIMPLE 0 #define PyBUF_WRITABLE 0x0001 /* we used to include an E, backwards compatible alias */ diff --git a/Lib/ctypes/test/test_pep3118.py b/Lib/ctypes/test/test_pep3118.py index fa6461f5460..ad13b016e76 100644 --- a/Lib/ctypes/test/test_pep3118.py +++ b/Lib/ctypes/test/test_pep3118.py @@ -25,14 +25,17 @@ class Test(unittest.TestCase): v = memoryview(ob) try: self.assertEqual(normalize(v.format), normalize(fmt)) - if shape is not None: + if shape: self.assertEqual(len(v), shape[0]) else: self.assertEqual(len(v) * sizeof(itemtp), sizeof(ob)) self.assertEqual(v.itemsize, sizeof(itemtp)) self.assertEqual(v.shape, shape) - # ctypes object always have a non-strided memory block - self.assertEqual(v.strides, None) + # XXX Issue #12851: PyCData_NewGetBuffer() must provide strides + # if requested. memoryview currently reconstructs missing + # stride information, so this assert will fail. + # self.assertEqual(v.strides, ()) + # they are always read/write self.assertFalse(v.readonly) @@ -52,14 +55,15 @@ class Test(unittest.TestCase): v = memoryview(ob) try: self.assertEqual(v.format, fmt) - if shape is not None: + if shape: self.assertEqual(len(v), shape[0]) else: self.assertEqual(len(v) * sizeof(itemtp), sizeof(ob)) self.assertEqual(v.itemsize, sizeof(itemtp)) self.assertEqual(v.shape, shape) - # ctypes object always have a non-strided memory block - self.assertEqual(v.strides, None) + # XXX Issue #12851 + # self.assertEqual(v.strides, ()) + # they are always read/write self.assertFalse(v.readonly) @@ -110,34 +114,34 @@ native_types = [ ## simple types - (c_char, "l:x:>l:y:}", None, BEPoint), - (LEPoint, "T{l:x:>l:y:}", None, POINTER(BEPoint)), - (POINTER(LEPoint), "&T{l:x:>l:y:}", (), BEPoint), + (LEPoint, "T{l:x:>l:y:}", (), POINTER(BEPoint)), + (POINTER(LEPoint), "&T{':STANDARD, + '=':STANDARD, + '!':STANDARD +} + +if struct: + for fmt in fmtdict['@']: + fmtdict['@'][fmt] = native_type_range(fmt) + +MEMORYVIEW = NATIVE.copy() +ARRAY = NATIVE.copy() +for k in NATIVE: + if not k in "bBhHiIlLfd": + del ARRAY[k] + +BYTEFMT = NATIVE.copy() +for k in NATIVE: + if not k in "Bbc": + del BYTEFMT[k] + +fmtdict['m'] = MEMORYVIEW +fmtdict['@m'] = MEMORYVIEW +fmtdict['a'] = ARRAY +fmtdict['b'] = BYTEFMT +fmtdict['@b'] = BYTEFMT + +# Capabilities of the test objects: +MODE = 0 +MULT = 1 +cap = { # format chars # multiplier + 'ndarray': (['', '@', '<', '>', '=', '!'], ['', '1', '2', '3']), + 'array': (['a'], ['']), + 'numpy': ([''], ['']), + 'memoryview': (['@m', 'm'], ['']), + 'bytefmt': (['@b', 'b'], ['']), +} + +def randrange_fmt(mode, char, obj): + """Return random item for a type specified by a mode and a single + format character.""" + x = randrange(*fmtdict[mode][char]) + if char == 'c': + x = bytes(chr(x), 'latin1') + if char == '?': + x = bool(x) + if char == 'f' or char == 'd': + x = struct.pack(char, x) + x = struct.unpack(char, x)[0] + if obj == 'numpy' and x == b'\x00': + # http://projects.scipy.org/numpy/ticket/1925 + x = b'\x01' + return x + +def gen_item(fmt, obj): + """Return single random item.""" + mode, chars = fmt.split('#') + x = [] + for c in chars: + x.append(randrange_fmt(mode, c, obj)) + return x[0] if len(x) == 1 else tuple(x) + +def gen_items(n, fmt, obj): + """Return a list of random items (or a scalar).""" + if n == 0: + return gen_item(fmt, obj) + lst = [0] * n + for i in range(n): + lst[i] = gen_item(fmt, obj) + return lst + +def struct_items(n, obj): + mode = choice(cap[obj][MODE]) + xfmt = mode + '#' + fmt = mode.strip('amb') + nmemb = randrange(2, 10) # number of struct members + for _ in range(nmemb): + char = choice(tuple(fmtdict[mode])) + multiplier = choice(cap[obj][MULT]) + xfmt += (char * int(multiplier if multiplier else 1)) + fmt += (multiplier + char) + items = gen_items(n, xfmt, obj) + item = gen_item(xfmt, obj) + return fmt, items, item + +def randitems(n, obj='ndarray', mode=None, char=None): + """Return random format, items, item.""" + if mode is None: + mode = choice(cap[obj][MODE]) + if char is None: + char = choice(tuple(fmtdict[mode])) + multiplier = choice(cap[obj][MULT]) + fmt = mode + '#' + char * int(multiplier if multiplier else 1) + items = gen_items(n, fmt, obj) + item = gen_item(fmt, obj) + fmt = mode.strip('amb') + multiplier + char + return fmt, items, item + +def iter_mode(n, obj='ndarray'): + """Iterate through supported mode/char combinations.""" + for mode in cap[obj][MODE]: + for char in fmtdict[mode]: + yield randitems(n, obj, mode, char) + +def iter_format(nitems, testobj='ndarray'): + """Yield (format, items, item) for all possible modes and format + characters plus one random compound format string.""" + for t in iter_mode(nitems, testobj): + yield t + if testobj != 'ndarray': + raise StopIteration + yield struct_items(nitems, testobj) + + +def is_byte_format(fmt): + return 'c' in fmt or 'b' in fmt or 'B' in fmt + +def is_memoryview_format(fmt): + """format suitable for memoryview""" + x = len(fmt) + return ((x == 1 or (x == 2 and fmt[0] == '@')) and + fmt[x-1] in MEMORYVIEW) + +NON_BYTE_FORMAT = [c for c in fmtdict['@'] if not is_byte_format(c)] + + +# ====================================================================== +# Multi-dimensional tolist(), slicing and slice assignments +# ====================================================================== + +def atomp(lst): + """Tuple items (representing structs) are regarded as atoms.""" + return not isinstance(lst, list) + +def listp(lst): + return isinstance(lst, list) + +def prod(lst): + """Product of list elements.""" + if len(lst) == 0: + return 0 + x = lst[0] + for v in lst[1:]: + x *= v + return x + +def strides_from_shape(ndim, shape, itemsize, layout): + """Calculate strides of a contiguous array. Layout is 'C' or + 'F' (Fortran).""" + if ndim == 0: + return () + if layout == 'C': + strides = list(shape[1:]) + [itemsize] + for i in range(ndim-2, -1, -1): + strides[i] *= strides[i+1] + else: + strides = [itemsize] + list(shape[:-1]) + for i in range(1, ndim): + strides[i] *= strides[i-1] + return strides + +def _ca(items, s): + """Convert flat item list to the nested list representation of a + multidimensional C array with shape 's'.""" + if atomp(items): + return items + if len(s) == 0: + return items[0] + lst = [0] * s[0] + stride = len(items) // s[0] if s[0] else 0 + for i in range(s[0]): + start = i*stride + lst[i] = _ca(items[start:start+stride], s[1:]) + return lst + +def _fa(items, s): + """Convert flat item list to the nested list representation of a + multidimensional Fortran array with shape 's'.""" + if atomp(items): + return items + if len(s) == 0: + return items[0] + lst = [0] * s[0] + stride = s[0] + for i in range(s[0]): + lst[i] = _fa(items[i::stride], s[1:]) + return lst + +def carray(items, shape): + if listp(items) and not 0 in shape and prod(shape) != len(items): + raise ValueError("prod(shape) != len(items)") + return _ca(items, shape) + +def farray(items, shape): + if listp(items) and not 0 in shape and prod(shape) != len(items): + raise ValueError("prod(shape) != len(items)") + return _fa(items, shape) + +def indices(shape): + """Generate all possible tuples of indices.""" + iterables = [range(v) for v in shape] + return product(*iterables) + +def getindex(ndim, ind, strides): + """Convert multi-dimensional index to the position in the flat list.""" + ret = 0 + for i in range(ndim): + ret += strides[i] * ind[i] + return ret + +def transpose(src, shape): + """Transpose flat item list that is regarded as a multi-dimensional + matrix defined by shape: dest...[k][j][i] = src[i][j][k]... """ + if not shape: + return src + ndim = len(shape) + sstrides = strides_from_shape(ndim, shape, 1, 'C') + dstrides = strides_from_shape(ndim, shape[::-1], 1, 'C') + dest = [0] * len(src) + for ind in indices(shape): + fr = getindex(ndim, ind, sstrides) + to = getindex(ndim, ind[::-1], dstrides) + dest[to] = src[fr] + return dest + +def _flatten(lst): + """flatten list""" + if lst == []: + return lst + if atomp(lst): + return [lst] + return _flatten(lst[0]) + _flatten(lst[1:]) + +def flatten(lst): + """flatten list or return scalar""" + if atomp(lst): # scalar + return lst + return _flatten(lst) + +def slice_shape(lst, slices): + """Get the shape of lst after slicing: slices is a list of slice + objects.""" + if atomp(lst): + return [] + return [len(lst[slices[0]])] + slice_shape(lst[0], slices[1:]) + +def multislice(lst, slices): + """Multi-dimensional slicing: slices is a list of slice objects.""" + if atomp(lst): + return lst + return [multislice(sublst, slices[1:]) for sublst in lst[slices[0]]] + +def m_assign(llst, rlst, lslices, rslices): + """Multi-dimensional slice assignment: llst and rlst are the operands, + lslices and rslices are lists of slice objects. llst and rlst must + have the same structure. + + For a two-dimensional example, this is not implemented in Python: + + llst[0:3:2, 0:3:2] = rlst[1:3:1, 1:3:1] + + Instead we write: + + lslices = [slice(0,3,2), slice(0,3,2)] + rslices = [slice(1,3,1), slice(1,3,1)] + multislice_assign(llst, rlst, lslices, rslices) + """ + if atomp(rlst): + return rlst + rlst = [m_assign(l, r, lslices[1:], rslices[1:]) + for l, r in zip(llst[lslices[0]], rlst[rslices[0]])] + llst[lslices[0]] = rlst + return llst + +def cmp_structure(llst, rlst, lslices, rslices): + """Compare the structure of llst[lslices] and rlst[rslices].""" + lshape = slice_shape(llst, lslices) + rshape = slice_shape(rlst, rslices) + if (len(lshape) != len(rshape)): + return -1 + for i in range(len(lshape)): + if lshape[i] != rshape[i]: + return -1 + if lshape[i] == 0: + return 0 + return 0 + +def multislice_assign(llst, rlst, lslices, rslices): + """Return llst after assigning: llst[lslices] = rlst[rslices]""" + if cmp_structure(llst, rlst, lslices, rslices) < 0: + raise ValueError("lvalue and rvalue have different structures") + return m_assign(llst, rlst, lslices, rslices) + + +# ====================================================================== +# Random structures +# ====================================================================== + +# +# PEP-3118 is very permissive with respect to the contents of a +# Py_buffer. In particular: +# +# - shape can be zero +# - strides can be any integer, including zero +# - offset can point to any location in the underlying +# memory block, provided that it is a multiple of +# itemsize. +# +# The functions in this section test and verify random structures +# in full generality. A structure is valid iff it fits in the +# underlying memory block. +# +# The structure 't' (short for 'tuple') is fully defined by: +# +# t = (memlen, itemsize, ndim, shape, strides, offset) +# + +def verify_structure(memlen, itemsize, ndim, shape, strides, offset): + """Verify that the parameters represent a valid array within + the bounds of the allocated memory: + char *mem: start of the physical memory block + memlen: length of the physical memory block + offset: (char *)buf - mem + """ + if offset % itemsize: + return False + if offset < 0 or offset+itemsize > memlen: + return False + if any(v % itemsize for v in strides): + return False + + if ndim <= 0: + return ndim == 0 and not shape and not strides + if 0 in shape: + return True + + imin = sum(strides[j]*(shape[j]-1) for j in range(ndim) + if strides[j] <= 0) + imax = sum(strides[j]*(shape[j]-1) for j in range(ndim) + if strides[j] > 0) + + return 0 <= offset+imin and offset+imax+itemsize <= memlen + +def get_item(lst, indices): + for i in indices: + lst = lst[i] + return lst + +def memory_index(indices, t): + """Location of an item in the underlying memory.""" + memlen, itemsize, ndim, shape, strides, offset = t + p = offset + for i in range(ndim): + p += strides[i]*indices[i] + return p + +def is_overlapping(t): + """The structure 't' is overlapping if at least one memory location + is visited twice while iterating through all possible tuples of + indices.""" + memlen, itemsize, ndim, shape, strides, offset = t + visited = 1<= 95 and valid: + minshape = 0 + elif n >= 90: + minshape = 1 + shape = [0] * ndim + + for i in range(ndim): + shape[i] = randrange(minshape, maxshape+1) + else: + ndim = len(shape) + + maxstride = 5 + n = randrange(100) + zero_stride = True if n >= 95 and n & 1 else False + + strides = [0] * ndim + strides[ndim-1] = itemsize * randrange(-maxstride, maxstride+1) + if not zero_stride and strides[ndim-1] == 0: + strides[ndim-1] = itemsize + + for i in range(ndim-2, -1, -1): + maxstride *= shape[i+1] if shape[i+1] else 1 + if zero_stride: + strides[i] = itemsize * randrange(-maxstride, maxstride+1) + else: + strides[i] = ((1,-1)[randrange(2)] * + itemsize * randrange(1, maxstride+1)) + + imin = imax = 0 + if not 0 in shape: + imin = sum(strides[j]*(shape[j]-1) for j in range(ndim) + if strides[j] <= 0) + imax = sum(strides[j]*(shape[j]-1) for j in range(ndim) + if strides[j] > 0) + + nitems = imax - imin + if valid: + offset = -imin * itemsize + memlen = offset + (imax+1) * itemsize + else: + memlen = (-imin + imax) * itemsize + offset = -imin-itemsize if randrange(2) == 0 else memlen + return memlen, itemsize, ndim, shape, strides, offset + +def randslice_from_slicelen(slicelen, listlen): + """Create a random slice of len slicelen that fits into listlen.""" + maxstart = listlen - slicelen + start = randrange(maxstart+1) + maxstep = (listlen - start) // slicelen if slicelen else 1 + step = randrange(1, maxstep+1) + stop = start + slicelen * step + s = slice(start, stop, step) + _, _, _, control = slice_indices(s, listlen) + if control != slicelen: + raise RuntimeError + return s + +def randslice_from_shape(ndim, shape): + """Create two sets of slices for an array x with shape 'shape' + such that shapeof(x[lslices]) == shapeof(x[rslices]).""" + lslices = [0] * ndim + rslices = [0] * ndim + for n in range(ndim): + l = shape[n] + slicelen = randrange(1, l+1) if l > 0 else 0 + lslices[n] = randslice_from_slicelen(slicelen, l) + rslices[n] = randslice_from_slicelen(slicelen, l) + return tuple(lslices), tuple(rslices) + +def rand_aligned_slices(maxdim=5, maxshape=16): + """Create (lshape, rshape, tuple(lslices), tuple(rslices)) such that + shapeof(x[lslices]) == shapeof(y[rslices]), where x is an array + with shape 'lshape' and y is an array with shape 'rshape'.""" + ndim = randrange(1, maxdim+1) + minshape = 2 + n = randrange(100) + if n >= 95: + minshape = 0 + elif n >= 90: + minshape = 1 + all_random = True if randrange(100) >= 80 else False + lshape = [0]*ndim; rshape = [0]*ndim + lslices = [0]*ndim; rslices = [0]*ndim + + for n in range(ndim): + small = randrange(minshape, maxshape+1) + big = randrange(minshape, maxshape+1) + if big < small: + big, small = small, big + + # Create a slice that fits the smaller value. + if all_random: + start = randrange(-small, small+1) + stop = randrange(-small, small+1) + step = (1,-1)[randrange(2)] * randrange(1, small+2) + s_small = slice(start, stop, step) + _, _, _, slicelen = slice_indices(s_small, small) + else: + slicelen = randrange(1, small+1) if small > 0 else 0 + s_small = randslice_from_slicelen(slicelen, small) + + # Create a slice of the same length for the bigger value. + s_big = randslice_from_slicelen(slicelen, big) + if randrange(2) == 0: + rshape[n], lshape[n] = big, small + rslices[n], lslices[n] = s_big, s_small + else: + rshape[n], lshape[n] = small, big + rslices[n], lslices[n] = s_small, s_big + + return lshape, rshape, tuple(lslices), tuple(rslices) + +def randitems_from_structure(fmt, t): + """Return a list of random items for structure 't' with format + 'fmtchar'.""" + memlen, itemsize, _, _, _, _ = t + return gen_items(memlen//itemsize, '#'+fmt, 'numpy') + +def ndarray_from_structure(items, fmt, t, flags=0): + """Return ndarray from the tuple returned by rand_structure()""" + memlen, itemsize, ndim, shape, strides, offset = t + return ndarray(items, shape=shape, strides=strides, format=fmt, + offset=offset, flags=ND_WRITABLE|flags) + +def numpy_array_from_structure(items, fmt, t): + """Return numpy_array from the tuple returned by rand_structure()""" + memlen, itemsize, ndim, shape, strides, offset = t + buf = bytearray(memlen) + for j, v in enumerate(items): + struct.pack_into(fmt, buf, j*itemsize, v) + return numpy_array(buffer=buf, shape=shape, strides=strides, + dtype=fmt, offset=offset) + + +# ====================================================================== +# memoryview casts +# ====================================================================== + +def cast_items(exporter, fmt, itemsize, shape=None): + """Interpret the raw memory of 'exporter' as a list of items with + size 'itemsize'. If shape=None, the new structure is assumed to + be 1-D with n * itemsize = bytelen. If shape is given, the usual + constraint for contiguous arrays prod(shape) * itemsize = bytelen + applies. On success, return (items, shape). If the constraints + cannot be met, return (None, None). If a chunk of bytes is interpreted + as NaN as a result of float conversion, return ('nan', None).""" + bytelen = exporter.nbytes + if shape: + if prod(shape) * itemsize != bytelen: + return None, shape + elif shape == []: + if exporter.ndim == 0 or itemsize != bytelen: + return None, shape + else: + n, r = divmod(bytelen, itemsize) + shape = [n] + if r != 0: + return None, shape + + mem = exporter.tobytes() + byteitems = [mem[i:i+itemsize] for i in range(0, len(mem), itemsize)] + + items = [] + for v in byteitems: + item = struct.unpack(fmt, v)[0] + if item != item: + return 'nan', shape + items.append(item) + + return (items, shape) if shape != [] else (items[0], shape) + +def gencastshapes(): + """Generate shapes to test casting.""" + for n in range(32): + yield [n] + ndim = randrange(4, 6) + minshape = 1 if randrange(100) > 80 else 2 + yield [randrange(minshape, 5) for _ in range(ndim)] + ndim = randrange(2, 4) + minshape = 1 if randrange(100) > 80 else 2 + yield [randrange(minshape, 5) for _ in range(ndim)] + + +# ====================================================================== +# Actual tests +# ====================================================================== + +def genslices(n): + """Generate all possible slices for a single dimension.""" + return product(range(-n, n+1), range(-n, n+1), range(-n, n+1)) + +def genslices_ndim(ndim, shape): + """Generate all possible slice tuples for 'shape'.""" + iterables = [genslices(shape[n]) for n in range(ndim)] + return product(*iterables) + +def rslice(n, allow_empty=False): + """Generate random slice for a single dimension of length n. + If zero=True, the slices may be empty, otherwise they will + be non-empty.""" + minlen = 0 if allow_empty or n == 0 else 1 + slicelen = randrange(minlen, n+1) + return randslice_from_slicelen(slicelen, n) + +def rslices(n, allow_empty=False): + """Generate random slices for a single dimension.""" + for _ in range(5): + yield rslice(n, allow_empty) + +def rslices_ndim(ndim, shape, iterations=5): + """Generate random slice tuples for 'shape'.""" + # non-empty slices + for _ in range(iterations): + yield tuple(rslice(shape[n]) for n in range(ndim)) + # possibly empty slices + for _ in range(iterations): + yield tuple(rslice(shape[n], allow_empty=True) for n in range(ndim)) + # invalid slices + yield tuple(slice(0,1,0) for _ in range(ndim)) + +def rpermutation(iterable, r=None): + pool = tuple(iterable) + r = len(pool) if r is None else r + yield tuple(sample(pool, r)) + +def ndarray_print(nd): + """Print ndarray for debugging.""" + try: + x = nd.tolist() + except (TypeError, NotImplementedError): + x = nd.tobytes() + if isinstance(nd, ndarray): + offset = nd.offset + flags = nd.flags + else: + offset = 'unknown' + flags = 'unknown' + print("ndarray(%s, shape=%s, strides=%s, suboffsets=%s, offset=%s, " + "format='%s', itemsize=%s, flags=%s)" % + (x, nd.shape, nd.strides, nd.suboffsets, offset, + nd.format, nd.itemsize, flags)) + sys.stdout.flush() + + +ITERATIONS = 100 +MAXDIM = 5 +MAXSHAPE = 10 + +if SHORT_TEST: + ITERATIONS = 10 + MAXDIM = 3 + MAXSHAPE = 4 + genslices = rslices + genslices_ndim = rslices_ndim + permutations = rpermutation + + +@unittest.skipUnless(struct, 'struct module required for this test.') +@unittest.skipUnless(ndarray, 'ndarray object required for this test') +class TestBufferProtocol(unittest.TestCase): + + def setUp(self): + self.sizeof_void_p = get_config_var('SIZEOF_VOID_P') + if not self.sizeof_void_p: + self.sizeof_void_p = 8 if architecture()[0] == '64bit' else 4 + + def verify(self, result, obj=-1, + itemsize={1}, fmt=-1, readonly={1}, + ndim={1}, shape=-1, strides=-1, + lst=-1, sliced=False, cast=False): + # Verify buffer contents against expected values. Default values + # are deliberately initialized to invalid types. + if shape: + expected_len = prod(shape)*itemsize + else: + if not fmt: # array has been implicitly cast to unsigned bytes + expected_len = len(lst) + else: # ndim = 0 + expected_len = itemsize + + # Reconstruct suboffsets from strides. Support for slicing + # could be added, but is currently only needed for test_getbuf(). + suboffsets = () + if result.suboffsets: + self.assertGreater(ndim, 0) + + suboffset0 = 0 + for n in range(1, ndim): + if shape[n] == 0: + break + if strides[n] <= 0: + suboffset0 += -strides[n] * (shape[n]-1) + + suboffsets = [suboffset0] + [-1 for v in range(ndim-1)] + + # Not correct if slicing has occurred in the first dimension. + stride0 = self.sizeof_void_p + if strides[0] < 0: + stride0 = -stride0 + strides = [stride0] + list(strides[1:]) + + self.assertIs(result.obj, obj) + self.assertEqual(result.nbytes, expected_len) + self.assertEqual(result.itemsize, itemsize) + self.assertEqual(result.format, fmt) + self.assertEqual(result.readonly, readonly) + self.assertEqual(result.ndim, ndim) + self.assertEqual(result.shape, tuple(shape)) + if not (sliced and suboffsets): + self.assertEqual(result.strides, tuple(strides)) + self.assertEqual(result.suboffsets, tuple(suboffsets)) + + if isinstance(result, ndarray) or is_memoryview_format(fmt): + rep = result.tolist() if fmt else result.tobytes() + self.assertEqual(rep, lst) + + if not fmt: # array has been cast to unsigned bytes, + return # the remaining tests won't work. + + # PyBuffer_GetPointer() is the definition how to access an item. + # If PyBuffer_GetPointer(indices) is correct for all possible + # combinations of indices, the buffer is correct. + # + # Also test tobytes() against the flattened 'lst', with all items + # packed to bytes. + if not cast: # casts chop up 'lst' in different ways + b = bytearray() + buf_err = None + for ind in indices(shape): + try: + item1 = get_pointer(result, ind) + item2 = get_item(lst, ind) + if isinstance(item2, tuple): + x = struct.pack(fmt, *item2) + else: + x = struct.pack(fmt, item2) + b.extend(x) + except BufferError: + buf_err = True # re-exporter does not provide full buffer + break + self.assertEqual(item1, item2) + + if not buf_err: + # test tobytes() + self.assertEqual(result.tobytes(), b) + + if not buf_err and is_memoryview_format(fmt): + + # lst := expected multi-dimensional logical representation + # flatten(lst) := elements in C-order + ff = fmt if fmt else 'B' + flattened = flatten(lst) + + # Rules for 'A': if the array is already contiguous, return + # the array unaltered. Otherwise, return a contiguous 'C' + # representation. + for order in ['C', 'F', 'A']: + expected = result + if order == 'F': + if not is_contiguous(result, 'A') or \ + is_contiguous(result, 'C'): + # For constructing the ndarray, convert the + # flattened logical representation to Fortran order. + trans = transpose(flattened, shape) + expected = ndarray(trans, shape=shape, format=ff, + flags=ND_FORTRAN) + else: # 'C', 'A' + if not is_contiguous(result, 'A') or \ + is_contiguous(result, 'F') and order == 'C': + # The flattened list is already in C-order. + expected = ndarray(flattened, shape=shape, format=ff) + contig = get_contiguous(result, PyBUF_READ, order) + contig = get_contiguous(result, PyBUF_READ, order) + self.assertEqual(contig.tobytes(), b) + self.assertTrue(cmp_contig(contig, expected)) + + if is_memoryview_format(fmt): + try: + m = memoryview(result) + except BufferError: # re-exporter does not provide full information + return + ex = result.obj if isinstance(result, memoryview) else result + self.assertIs(m.obj, ex) + self.assertEqual(m.nbytes, expected_len) + self.assertEqual(m.itemsize, itemsize) + self.assertEqual(m.format, fmt) + self.assertEqual(m.readonly, readonly) + self.assertEqual(m.ndim, ndim) + self.assertEqual(m.shape, tuple(shape)) + if not (sliced and suboffsets): + self.assertEqual(m.strides, tuple(strides)) + self.assertEqual(m.suboffsets, tuple(suboffsets)) + + n = 1 if ndim == 0 else len(lst) + self.assertEqual(len(m), n) + + rep = result.tolist() if fmt else result.tobytes() + self.assertEqual(rep, lst) + self.assertEqual(m, result) + + def verify_getbuf(self, orig_ex, ex, req, sliced=False): + def simple_fmt(ex): + return ex.format == '' or ex.format == 'B' + def match(req, flag): + return ((req&flag) == flag) + + if (# writable request to read-only exporter + (ex.readonly and match(req, PyBUF_WRITABLE)) or + # cannot match explicit contiguity request + (match(req, PyBUF_C_CONTIGUOUS) and not ex.c_contiguous) or + (match(req, PyBUF_F_CONTIGUOUS) and not ex.f_contiguous) or + (match(req, PyBUF_ANY_CONTIGUOUS) and not ex.contiguous) or + # buffer needs suboffsets + (not match(req, PyBUF_INDIRECT) and ex.suboffsets) or + # buffer without strides must be C-contiguous + (not match(req, PyBUF_STRIDES) and not ex.c_contiguous) or + # PyBUF_SIMPLE|PyBUF_FORMAT and PyBUF_WRITABLE|PyBUF_FORMAT + (not match(req, PyBUF_ND) and match(req, PyBUF_FORMAT))): + + self.assertRaises(BufferError, ndarray, ex, getbuf=req) + return + + if isinstance(ex, ndarray) or is_memoryview_format(ex.format): + lst = ex.tolist() + else: + nd = ndarray(ex, getbuf=PyBUF_FULL_RO) + lst = nd.tolist() + + # The consumer may have requested default values or a NULL format. + ro = 0 if match(req, PyBUF_WRITABLE) else ex.readonly + fmt = ex.format + itemsize = ex.itemsize + ndim = ex.ndim + if not match(req, PyBUF_FORMAT): + # itemsize refers to the original itemsize before the cast. + # The equality product(shape) * itemsize = len still holds. + # The equality calcsize(format) = itemsize does _not_ hold. + fmt = '' + lst = orig_ex.tobytes() # Issue 12834 + if not match(req, PyBUF_ND): + ndim = 1 + shape = orig_ex.shape if match(req, PyBUF_ND) else () + strides = orig_ex.strides if match(req, PyBUF_STRIDES) else () + + nd = ndarray(ex, getbuf=req) + self.verify(nd, obj=ex, + itemsize=itemsize, fmt=fmt, readonly=ro, + ndim=ndim, shape=shape, strides=strides, + lst=lst, sliced=sliced) + + def test_ndarray_getbuf(self): + requests = ( + # distinct flags + PyBUF_INDIRECT, PyBUF_STRIDES, PyBUF_ND, PyBUF_SIMPLE, + PyBUF_C_CONTIGUOUS, PyBUF_F_CONTIGUOUS, PyBUF_ANY_CONTIGUOUS, + # compound requests + PyBUF_FULL, PyBUF_FULL_RO, + PyBUF_RECORDS, PyBUF_RECORDS_RO, + PyBUF_STRIDED, PyBUF_STRIDED_RO, + PyBUF_CONTIG, PyBUF_CONTIG_RO, + ) + # items and format + items_fmt = ( + ([True if x % 2 else False for x in range(12)], '?'), + ([1,2,3,4,5,6,7,8,9,10,11,12], 'b'), + ([1,2,3,4,5,6,7,8,9,10,11,12], 'B'), + ([(2**31-x) if x % 2 else (-2**31+x) for x in range(12)], 'l') + ) + # shape, strides, offset + structure = ( + ([], [], 0), + ([12], [], 0), + ([12], [-1], 11), + ([6], [2], 0), + ([6], [-2], 11), + ([3, 4], [], 0), + ([3, 4], [-4, -1], 11), + ([2, 2], [4, 1], 4), + ([2, 2], [-4, -1], 8) + ) + # ndarray creation flags + ndflags = ( + 0, ND_WRITABLE, ND_FORTRAN, ND_FORTRAN|ND_WRITABLE, + ND_PIL, ND_PIL|ND_WRITABLE + ) + # flags that can actually be used as flags + real_flags = (0, PyBUF_WRITABLE, PyBUF_FORMAT, + PyBUF_WRITABLE|PyBUF_FORMAT) + + for items, fmt in items_fmt: + itemsize = struct.calcsize(fmt) + for shape, strides, offset in structure: + strides = [v * itemsize for v in strides] + offset *= itemsize + for flags in ndflags: + + if strides and (flags&ND_FORTRAN): + continue + if not shape and (flags&ND_PIL): + continue + + _items = items if shape else items[0] + ex1 = ndarray(_items, format=fmt, flags=flags, + shape=shape, strides=strides, offset=offset) + ex2 = ex1[::-2] if shape else None + + m1 = memoryview(ex1) + if ex2: + m2 = memoryview(ex2) + if ex1.ndim == 0 or (ex1.ndim == 1 and shape and strides): + self.assertEqual(m1, ex1) + if ex2 and ex2.ndim == 1 and shape and strides: + self.assertEqual(m2, ex2) + + for req in requests: + for bits in real_flags: + self.verify_getbuf(ex1, ex1, req|bits) + self.verify_getbuf(ex1, m1, req|bits) + if ex2: + self.verify_getbuf(ex2, ex2, req|bits, + sliced=True) + self.verify_getbuf(ex2, m2, req|bits, + sliced=True) + + items = [1,2,3,4,5,6,7,8,9,10,11,12] + + # ND_GETBUF_FAIL + ex = ndarray(items, shape=[12], flags=ND_GETBUF_FAIL) + self.assertRaises(BufferError, ndarray, ex) + + # Request complex structure from a simple exporter. In this + # particular case the test object is not PEP-3118 compliant. + base = ndarray([9], [1]) + ex = ndarray(base, getbuf=PyBUF_SIMPLE) + self.assertRaises(BufferError, ndarray, ex, getbuf=PyBUF_WRITABLE) + self.assertRaises(BufferError, ndarray, ex, getbuf=PyBUF_ND) + self.assertRaises(BufferError, ndarray, ex, getbuf=PyBUF_STRIDES) + self.assertRaises(BufferError, ndarray, ex, getbuf=PyBUF_C_CONTIGUOUS) + self.assertRaises(BufferError, ndarray, ex, getbuf=PyBUF_F_CONTIGUOUS) + self.assertRaises(BufferError, ndarray, ex, getbuf=PyBUF_ANY_CONTIGUOUS) + nd = ndarray(ex, getbuf=PyBUF_SIMPLE) + + def test_ndarray_exceptions(self): + nd = ndarray([9], [1]) + ndm = ndarray([9], [1], flags=ND_VAREXPORT) + + # Initialization of a new ndarray or mutation of an existing array. + for c in (ndarray, nd.push, ndm.push): + # Invalid types. + self.assertRaises(TypeError, c, {1,2,3}) + self.assertRaises(TypeError, c, [1,2,'3']) + self.assertRaises(TypeError, c, [1,2,(3,4)]) + self.assertRaises(TypeError, c, [1,2,3], shape={3}) + self.assertRaises(TypeError, c, [1,2,3], shape=[3], strides={1}) + self.assertRaises(TypeError, c, [1,2,3], shape=[3], offset=[]) + self.assertRaises(TypeError, c, [1], shape=[1], format={}) + self.assertRaises(TypeError, c, [1], shape=[1], flags={}) + self.assertRaises(TypeError, c, [1], shape=[1], getbuf={}) + + # ND_FORTRAN flag is only valid without strides. + self.assertRaises(TypeError, c, [1], shape=[1], strides=[1], + flags=ND_FORTRAN) + + # ND_PIL flag is only valid with ndim > 0. + self.assertRaises(TypeError, c, [1], shape=[], flags=ND_PIL) + + # Invalid items. + self.assertRaises(ValueError, c, [], shape=[1]) + self.assertRaises(ValueError, c, ['XXX'], shape=[1], format="L") + # Invalid combination of items and format. + self.assertRaises(struct.error, c, [1000], shape=[1], format="B") + self.assertRaises(ValueError, c, [1,(2,3)], shape=[2], format="B") + self.assertRaises(ValueError, c, [1,2,3], shape=[3], format="QL") + + # Invalid ndim. + n = ND_MAX_NDIM+1 + self.assertRaises(ValueError, c, [1]*n, shape=[1]*n) + + # Invalid shape. + self.assertRaises(ValueError, c, [1], shape=[-1]) + self.assertRaises(ValueError, c, [1,2,3], shape=['3']) + self.assertRaises(OverflowError, c, [1], shape=[2**128]) + # prod(shape) * itemsize != len(items) + self.assertRaises(ValueError, c, [1,2,3,4,5], shape=[2,2], offset=3) + + # Invalid strides. + self.assertRaises(ValueError, c, [1,2,3], shape=[3], strides=['1']) + self.assertRaises(OverflowError, c, [1], shape=[1], + strides=[2**128]) + + # Invalid combination of strides and shape. + self.assertRaises(ValueError, c, [1,2], shape=[2,1], strides=[1]) + # Invalid combination of strides and format. + self.assertRaises(ValueError, c, [1,2,3,4], shape=[2], strides=[3], + format="L") + + # Invalid offset. + self.assertRaises(ValueError, c, [1,2,3], shape=[3], offset=4) + self.assertRaises(ValueError, c, [1,2,3], shape=[1], offset=3, + format="L") + + # Invalid format. + self.assertRaises(ValueError, c, [1,2,3], shape=[3], format="") + self.assertRaises(struct.error, c, [(1,2,3)], shape=[1], + format="@#$") + + # Striding out of the memory bounds. + items = [1,2,3,4,5,6,7,8,9,10] + self.assertRaises(ValueError, c, items, shape=[2,3], + strides=[-3, -2], offset=5) + + # Constructing consumer: format argument invalid. + self.assertRaises(TypeError, c, bytearray(), format="Q") + + # Constructing original base object: getbuf argument invalid. + self.assertRaises(TypeError, c, [1], shape=[1], getbuf=PyBUF_FULL) + + # Shape argument is mandatory for original base objects. + self.assertRaises(TypeError, c, [1]) + + + # PyBUF_WRITABLE request to read-only provider. + self.assertRaises(BufferError, ndarray, b'123', getbuf=PyBUF_WRITABLE) + + # ND_VAREXPORT can only be specified during construction. + nd = ndarray([9], [1], flags=ND_VAREXPORT) + self.assertRaises(ValueError, nd.push, [1], [1], flags=ND_VAREXPORT) + + # Invalid operation for consumers: push/pop + nd = ndarray(b'123') + self.assertRaises(BufferError, nd.push, [1], [1]) + self.assertRaises(BufferError, nd.pop) + + # ND_VAREXPORT not set: push/pop fail with exported buffers + nd = ndarray([9], [1]) + nd.push([1], [1]) + m = memoryview(nd) + self.assertRaises(BufferError, nd.push, [1], [1]) + self.assertRaises(BufferError, nd.pop) + m.release() + nd.pop() + + # Single remaining buffer: pop fails + self.assertRaises(BufferError, nd.pop) + del nd + + # get_pointer() + self.assertRaises(TypeError, get_pointer, {}, [1,2,3]) + self.assertRaises(TypeError, get_pointer, b'123', {}) + + nd = ndarray(list(range(100)), shape=[1]*100) + self.assertRaises(ValueError, get_pointer, nd, [5]) + + nd = ndarray(list(range(12)), shape=[3,4]) + self.assertRaises(ValueError, get_pointer, nd, [2,3,4]) + self.assertRaises(ValueError, get_pointer, nd, [3,3]) + self.assertRaises(ValueError, get_pointer, nd, [-3,3]) + self.assertRaises(OverflowError, get_pointer, nd, [1<<64,3]) + + # tolist() needs format + ex = ndarray([1,2,3], shape=[3], format='L') + nd = ndarray(ex, getbuf=PyBUF_SIMPLE) + self.assertRaises(ValueError, nd.tolist) + + # memoryview_from_buffer() + ex1 = ndarray([1,2,3], shape=[3], format='L') + ex2 = ndarray(ex1) + nd = ndarray(ex2) + self.assertRaises(TypeError, nd.memoryview_from_buffer) + + nd = ndarray([(1,)*200], shape=[1], format='L'*200) + self.assertRaises(TypeError, nd.memoryview_from_buffer) + + n = ND_MAX_NDIM + nd = ndarray(list(range(n)), shape=[1]*n) + self.assertRaises(ValueError, nd.memoryview_from_buffer) + + # get_contiguous() + nd = ndarray([1], shape=[1]) + self.assertRaises(TypeError, get_contiguous, 1, 2, 3, 4, 5) + self.assertRaises(TypeError, get_contiguous, nd, "xyz", 'C') + self.assertRaises(OverflowError, get_contiguous, nd, 2**64, 'C') + self.assertRaises(TypeError, get_contiguous, nd, PyBUF_READ, 961) + self.assertRaises(UnicodeEncodeError, get_contiguous, nd, PyBUF_READ, + '\u2007') + + # cmp_contig() + nd = ndarray([1], shape=[1]) + self.assertRaises(TypeError, cmp_contig, 1, 2, 3, 4, 5) + self.assertRaises(TypeError, cmp_contig, {}, nd) + self.assertRaises(TypeError, cmp_contig, nd, {}) + + # is_contiguous() + nd = ndarray([1], shape=[1]) + self.assertRaises(TypeError, is_contiguous, 1, 2, 3, 4, 5) + self.assertRaises(TypeError, is_contiguous, {}, 'A') + self.assertRaises(TypeError, is_contiguous, nd, 201) + + def test_ndarray_linked_list(self): + for perm in permutations(range(5)): + m = [0]*5 + nd = ndarray([1,2,3], shape=[3], flags=ND_VAREXPORT) + m[0] = memoryview(nd) + + for i in range(1, 5): + nd.push([1,2,3], shape=[3]) + m[i] = memoryview(nd) + + for i in range(5): + m[perm[i]].release() + + self.assertRaises(BufferError, nd.pop) + del nd + + def test_ndarray_format_scalar(self): + # ndim = 0: scalar + for fmt, scalar, _ in iter_format(0): + itemsize = struct.calcsize(fmt) + nd = ndarray(scalar, shape=(), format=fmt) + self.verify(nd, obj=None, + itemsize=itemsize, fmt=fmt, readonly=1, + ndim=0, shape=(), strides=(), + lst=scalar) + + def test_ndarray_format_shape(self): + # ndim = 1, shape = [n] + nitems = randrange(1, 10) + for fmt, items, _ in iter_format(nitems): + itemsize = struct.calcsize(fmt) + for flags in (0, ND_PIL): + nd = ndarray(items, shape=[nitems], format=fmt, flags=flags) + self.verify(nd, obj=None, + itemsize=itemsize, fmt=fmt, readonly=1, + ndim=1, shape=(nitems,), strides=(itemsize,), + lst=items) + + def test_ndarray_format_strides(self): + # ndim = 1, strides + nitems = randrange(1, 30) + for fmt, items, _ in iter_format(nitems): + itemsize = struct.calcsize(fmt) + for step in range(-5, 5): + if step == 0: + continue + + shape = [len(items[::step])] + strides = [step*itemsize] + offset = itemsize*(nitems-1) if step < 0 else 0 + + for flags in (0, ND_PIL): + nd = ndarray(items, shape=shape, strides=strides, + format=fmt, offset=offset, flags=flags) + self.verify(nd, obj=None, + itemsize=itemsize, fmt=fmt, readonly=1, + ndim=1, shape=shape, strides=strides, + lst=items[::step]) + + def test_ndarray_fortran(self): + items = [1,2,3,4,5,6,7,8,9,10,11,12] + ex = ndarray(items, shape=(3, 4), strides=(1, 3)) + nd = ndarray(ex, getbuf=PyBUF_F_CONTIGUOUS|PyBUF_FORMAT) + self.assertEqual(nd.tolist(), farray(items, (3, 4))) + + def test_ndarray_multidim(self): + for ndim in range(5): + shape_t = [randrange(2, 10) for _ in range(ndim)] + nitems = prod(shape_t) + for shape in permutations(shape_t): + + fmt, items, _ = randitems(nitems) + itemsize = struct.calcsize(fmt) + + for flags in (0, ND_PIL): + if ndim == 0 and flags == ND_PIL: + continue + + # C array + nd = ndarray(items, shape=shape, format=fmt, flags=flags) + + strides = strides_from_shape(ndim, shape, itemsize, 'C') + lst = carray(items, shape) + self.verify(nd, obj=None, + itemsize=itemsize, fmt=fmt, readonly=1, + ndim=ndim, shape=shape, strides=strides, + lst=lst) + + if is_memoryview_format(fmt): + # memoryview: reconstruct strides + ex = ndarray(items, shape=shape, format=fmt) + nd = ndarray(ex, getbuf=PyBUF_CONTIG_RO|PyBUF_FORMAT) + self.assertTrue(nd.strides == ()) + mv = nd.memoryview_from_buffer() + self.verify(mv, obj=None, + itemsize=itemsize, fmt=fmt, readonly=1, + ndim=ndim, shape=shape, strides=strides, + lst=lst) + + # Fortran array + nd = ndarray(items, shape=shape, format=fmt, + flags=flags|ND_FORTRAN) + + strides = strides_from_shape(ndim, shape, itemsize, 'F') + lst = farray(items, shape) + self.verify(nd, obj=None, + itemsize=itemsize, fmt=fmt, readonly=1, + ndim=ndim, shape=shape, strides=strides, + lst=lst) + + def test_ndarray_index_invalid(self): + # not writable + nd = ndarray([1], shape=[1]) + self.assertRaises(TypeError, nd.__setitem__, 1, 8) + mv = memoryview(nd) + self.assertEqual(mv, nd) + self.assertRaises(TypeError, mv.__setitem__, 1, 8) + + # cannot be deleted + nd = ndarray([1], shape=[1], flags=ND_WRITABLE) + self.assertRaises(TypeError, nd.__delitem__, 1) + mv = memoryview(nd) + self.assertEqual(mv, nd) + self.assertRaises(TypeError, mv.__delitem__, 1) + + # overflow + nd = ndarray([1], shape=[1], flags=ND_WRITABLE) + self.assertRaises(OverflowError, nd.__getitem__, 1<<64) + self.assertRaises(OverflowError, nd.__setitem__, 1<<64, 8) + mv = memoryview(nd) + self.assertEqual(mv, nd) + self.assertRaises(IndexError, mv.__getitem__, 1<<64) + self.assertRaises(IndexError, mv.__setitem__, 1<<64, 8) + + # format + items = [1,2,3,4,5,6,7,8] + nd = ndarray(items, shape=[len(items)], format="B", flags=ND_WRITABLE) + self.assertRaises(struct.error, nd.__setitem__, 2, 300) + self.assertRaises(ValueError, nd.__setitem__, 1, (100, 200)) + mv = memoryview(nd) + self.assertEqual(mv, nd) + self.assertRaises(ValueError, mv.__setitem__, 2, 300) + self.assertRaises(TypeError, mv.__setitem__, 1, (100, 200)) + + items = [(1,2), (3,4), (5,6)] + nd = ndarray(items, shape=[len(items)], format="LQ", flags=ND_WRITABLE) + self.assertRaises(ValueError, nd.__setitem__, 2, 300) + self.assertRaises(struct.error, nd.__setitem__, 1, (b'\x001', 200)) + + def test_ndarray_index_scalar(self): + # scalar + nd = ndarray(1, shape=(), flags=ND_WRITABLE) + mv = memoryview(nd) + self.assertEqual(mv, nd) + + x = nd[()]; self.assertEqual(x, 1) + x = nd[...]; self.assertEqual(x.tolist(), nd.tolist()) + + x = mv[()]; self.assertEqual(x, 1) + x = mv[...]; self.assertEqual(x.tolist(), nd.tolist()) + + self.assertRaises(TypeError, nd.__getitem__, 0) + self.assertRaises(TypeError, mv.__getitem__, 0) + self.assertRaises(TypeError, nd.__setitem__, 0, 8) + self.assertRaises(TypeError, mv.__setitem__, 0, 8) + + self.assertEqual(nd.tolist(), 1) + self.assertEqual(mv.tolist(), 1) + + nd[()] = 9; self.assertEqual(nd.tolist(), 9) + mv[()] = 9; self.assertEqual(mv.tolist(), 9) + + nd[...] = 5; self.assertEqual(nd.tolist(), 5) + mv[...] = 5; self.assertEqual(mv.tolist(), 5) + + def test_ndarray_index_null_strides(self): + ex = ndarray(list(range(2*4)), shape=[2, 4], flags=ND_WRITABLE) + nd = ndarray(ex, getbuf=PyBUF_CONTIG) + + # Sub-views are only possible for full exporters. + self.assertRaises(BufferError, nd.__getitem__, 1) + # Same for slices. + self.assertRaises(BufferError, nd.__getitem__, slice(3,5,1)) + + def test_ndarray_index_getitem_single(self): + # getitem + for fmt, items, _ in iter_format(5): + nd = ndarray(items, shape=[5], format=fmt) + for i in range(-5, 5): + self.assertEqual(nd[i], items[i]) + + self.assertRaises(IndexError, nd.__getitem__, -6) + self.assertRaises(IndexError, nd.__getitem__, 5) + + if is_memoryview_format(fmt): + mv = memoryview(nd) + self.assertEqual(mv, nd) + for i in range(-5, 5): + self.assertEqual(mv[i], items[i]) + + self.assertRaises(IndexError, mv.__getitem__, -6) + self.assertRaises(IndexError, mv.__getitem__, 5) + + # getitem with null strides + for fmt, items, _ in iter_format(5): + ex = ndarray(items, shape=[5], flags=ND_WRITABLE, format=fmt) + nd = ndarray(ex, getbuf=PyBUF_CONTIG|PyBUF_FORMAT) + + for i in range(-5, 5): + self.assertEqual(nd[i], items[i]) + + if is_memoryview_format(fmt): + mv = nd.memoryview_from_buffer() + self.assertIs(mv.__eq__(nd), NotImplemented) + for i in range(-5, 5): + self.assertEqual(mv[i], items[i]) + + # getitem with null format + items = [1,2,3,4,5] + ex = ndarray(items, shape=[5]) + nd = ndarray(ex, getbuf=PyBUF_CONTIG_RO) + for i in range(-5, 5): + self.assertEqual(nd[i], items[i]) + + # getitem with null shape/strides/format + items = [1,2,3,4,5] + ex = ndarray(items, shape=[5]) + nd = ndarray(ex, getbuf=PyBUF_SIMPLE) + + for i in range(-5, 5): + self.assertEqual(nd[i], items[i]) + + def test_ndarray_index_setitem_single(self): + # assign single value + for fmt, items, single_item in iter_format(5): + nd = ndarray(items, shape=[5], format=fmt, flags=ND_WRITABLE) + for i in range(5): + items[i] = single_item + nd[i] = single_item + self.assertEqual(nd.tolist(), items) + + self.assertRaises(IndexError, nd.__setitem__, -6, single_item) + self.assertRaises(IndexError, nd.__setitem__, 5, single_item) + + if not is_memoryview_format(fmt): + continue + + nd = ndarray(items, shape=[5], format=fmt, flags=ND_WRITABLE) + mv = memoryview(nd) + self.assertEqual(mv, nd) + for i in range(5): + items[i] = single_item + mv[i] = single_item + self.assertEqual(mv.tolist(), items) + + self.assertRaises(IndexError, mv.__setitem__, -6, single_item) + self.assertRaises(IndexError, mv.__setitem__, 5, single_item) + + + # assign single value: lobject = robject + for fmt, items, single_item in iter_format(5): + nd = ndarray(items, shape=[5], format=fmt, flags=ND_WRITABLE) + for i in range(-5, 4): + items[i] = items[i+1] + nd[i] = nd[i+1] + self.assertEqual(nd.tolist(), items) + + if not is_memoryview_format(fmt): + continue + + nd = ndarray(items, shape=[5], format=fmt, flags=ND_WRITABLE) + mv = memoryview(nd) + self.assertEqual(mv, nd) + for i in range(-5, 4): + items[i] = items[i+1] + mv[i] = mv[i+1] + self.assertEqual(mv.tolist(), items) + + def test_ndarray_index_getitem_multidim(self): + shape_t = (2, 3, 5) + nitems = prod(shape_t) + for shape in permutations(shape_t): + + fmt, items, _ = randitems(nitems) + + for flags in (0, ND_PIL): + # C array + nd = ndarray(items, shape=shape, format=fmt, flags=flags) + lst = carray(items, shape) + + for i in range(-shape[0], shape[0]): + self.assertEqual(lst[i], nd[i].tolist()) + for j in range(-shape[1], shape[1]): + self.assertEqual(lst[i][j], nd[i][j].tolist()) + for k in range(-shape[2], shape[2]): + self.assertEqual(lst[i][j][k], nd[i][j][k]) + + # Fortran array + nd = ndarray(items, shape=shape, format=fmt, + flags=flags|ND_FORTRAN) + lst = farray(items, shape) + + for i in range(-shape[0], shape[0]): + self.assertEqual(lst[i], nd[i].tolist()) + for j in range(-shape[1], shape[1]): + self.assertEqual(lst[i][j], nd[i][j].tolist()) + for k in range(shape[2], shape[2]): + self.assertEqual(lst[i][j][k], nd[i][j][k]) + + def test_ndarray_sequence(self): + nd = ndarray(1, shape=()) + self.assertRaises(TypeError, eval, "1 in nd", locals()) + mv = memoryview(nd) + self.assertEqual(mv, nd) + self.assertRaises(TypeError, eval, "1 in mv", locals()) + + for fmt, items, _ in iter_format(5): + nd = ndarray(items, shape=[5], format=fmt) + for i, v in enumerate(nd): + self.assertEqual(v, items[i]) + self.assertTrue(v in nd) + + if is_memoryview_format(fmt): + mv = memoryview(nd) + for i, v in enumerate(mv): + self.assertEqual(v, items[i]) + self.assertTrue(v in mv) + + def test_ndarray_slice_invalid(self): + items = [1,2,3,4,5,6,7,8] + + # rvalue is not an exporter + xl = ndarray(items, shape=[8], flags=ND_WRITABLE) + ml = memoryview(xl) + self.assertRaises(TypeError, xl.__setitem__, slice(0,8,1), items) + self.assertRaises(TypeError, ml.__setitem__, slice(0,8,1), items) + + # rvalue is not a full exporter + xl = ndarray(items, shape=[8], flags=ND_WRITABLE) + ex = ndarray(items, shape=[8], flags=ND_WRITABLE) + xr = ndarray(ex, getbuf=PyBUF_ND) + self.assertRaises(BufferError, xl.__setitem__, slice(0,8,1), xr) + + # zero step + nd = ndarray(items, shape=[8], format="L", flags=ND_WRITABLE) + mv = memoryview(nd) + self.assertRaises(ValueError, nd.__getitem__, slice(0,1,0)) + self.assertRaises(ValueError, mv.__getitem__, slice(0,1,0)) + + nd = ndarray(items, shape=[2,4], format="L", flags=ND_WRITABLE) + mv = memoryview(nd) + + self.assertRaises(ValueError, nd.__getitem__, + (slice(0,1,1), slice(0,1,0))) + self.assertRaises(ValueError, nd.__getitem__, + (slice(0,1,0), slice(0,1,1))) + self.assertRaises(TypeError, nd.__getitem__, "@%$") + self.assertRaises(TypeError, nd.__getitem__, ("@%$", slice(0,1,1))) + self.assertRaises(TypeError, nd.__getitem__, (slice(0,1,1), {})) + + # memoryview: not implemented + self.assertRaises(NotImplementedError, mv.__getitem__, + (slice(0,1,1), slice(0,1,0))) + self.assertRaises(TypeError, mv.__getitem__, "@%$") + + # differing format + xl = ndarray(items, shape=[8], format="B", flags=ND_WRITABLE) + xr = ndarray(items, shape=[8], format="b") + ml = memoryview(xl) + mr = memoryview(xr) + self.assertRaises(ValueError, xl.__setitem__, slice(0,1,1), xr[7:8]) + self.assertEqual(xl.tolist(), items) + self.assertRaises(ValueError, ml.__setitem__, slice(0,1,1), mr[7:8]) + self.assertEqual(ml.tolist(), items) + + # differing itemsize + xl = ndarray(items, shape=[8], format="B", flags=ND_WRITABLE) + yr = ndarray(items, shape=[8], format="L") + ml = memoryview(xl) + mr = memoryview(xr) + self.assertRaises(ValueError, xl.__setitem__, slice(0,1,1), xr[7:8]) + self.assertEqual(xl.tolist(), items) + self.assertRaises(ValueError, ml.__setitem__, slice(0,1,1), mr[7:8]) + self.assertEqual(ml.tolist(), items) + + # differing ndim + xl = ndarray(items, shape=[2, 4], format="b", flags=ND_WRITABLE) + xr = ndarray(items, shape=[8], format="b") + ml = memoryview(xl) + mr = memoryview(xr) + self.assertRaises(ValueError, xl.__setitem__, slice(0,1,1), xr[7:8]) + self.assertEqual(xl.tolist(), [[1,2,3,4], [5,6,7,8]]) + self.assertRaises(NotImplementedError, ml.__setitem__, slice(0,1,1), + mr[7:8]) + + # differing shape + xl = ndarray(items, shape=[8], format="b", flags=ND_WRITABLE) + xr = ndarray(items, shape=[8], format="b") + ml = memoryview(xl) + mr = memoryview(xr) + self.assertRaises(ValueError, xl.__setitem__, slice(0,2,1), xr[7:8]) + self.assertEqual(xl.tolist(), items) + self.assertRaises(ValueError, ml.__setitem__, slice(0,2,1), mr[7:8]) + self.assertEqual(ml.tolist(), items) + + # _testbuffer.c module functions + self.assertRaises(TypeError, slice_indices, slice(0,1,2), {}) + self.assertRaises(TypeError, slice_indices, "###########", 1) + self.assertRaises(ValueError, slice_indices, slice(0,1,0), 4) + + x = ndarray(items, shape=[8], format="b", flags=ND_PIL) + self.assertRaises(TypeError, x.add_suboffsets) + + ex = ndarray(items, shape=[8], format="B") + x = ndarray(ex, getbuf=PyBUF_SIMPLE) + self.assertRaises(TypeError, x.add_suboffsets) + + def test_ndarray_slice_zero_shape(self): + items = [1,2,3,4,5,6,7,8,9,10,11,12] + + x = ndarray(items, shape=[12], format="L", flags=ND_WRITABLE) + y = ndarray(items, shape=[12], format="L") + x[4:4] = y[9:9] + self.assertEqual(x.tolist(), items) + + ml = memoryview(x) + mr = memoryview(y) + self.assertEqual(ml, x) + self.assertEqual(ml, y) + ml[4:4] = mr[9:9] + self.assertEqual(ml.tolist(), items) + + x = ndarray(items, shape=[3, 4], format="L", flags=ND_WRITABLE) + y = ndarray(items, shape=[4, 3], format="L") + x[1:2, 2:2] = y[1:2, 3:3] + self.assertEqual(x.tolist(), carray(items, [3, 4])) + + def test_ndarray_slice_multidim(self): + shape_t = (2, 3, 5) + ndim = len(shape_t) + nitems = prod(shape_t) + for shape in permutations(shape_t): + + fmt, items, _ = randitems(nitems) + itemsize = struct.calcsize(fmt) + + for flags in (0, ND_PIL): + nd = ndarray(items, shape=shape, format=fmt, flags=flags) + lst = carray(items, shape) + + for slices in rslices_ndim(ndim, shape): + + listerr = None + try: + sliced = multislice(lst, slices) + except Exception as e: + listerr = e.__class__ + + nderr = None + try: + ndsliced = nd[slices] + except Exception as e: + nderr = e.__class__ + + if nderr or listerr: + self.assertIs(nderr, listerr) + else: + self.assertEqual(ndsliced.tolist(), sliced) + + def test_ndarray_slice_redundant_suboffsets(self): + shape_t = (2, 3, 5, 2) + ndim = len(shape_t) + nitems = prod(shape_t) + for shape in permutations(shape_t): + + fmt, items, _ = randitems(nitems) + itemsize = struct.calcsize(fmt) + + nd = ndarray(items, shape=shape, format=fmt) + nd.add_suboffsets() + ex = ndarray(items, shape=shape, format=fmt) + ex.add_suboffsets() + mv = memoryview(ex) + lst = carray(items, shape) + + for slices in rslices_ndim(ndim, shape): + + listerr = None + try: + sliced = multislice(lst, slices) + except Exception as e: + listerr = e.__class__ + + nderr = None + try: + ndsliced = nd[slices] + except Exception as e: + nderr = e.__class__ + + if nderr or listerr: + self.assertIs(nderr, listerr) + else: + self.assertEqual(ndsliced.tolist(), sliced) + + def test_ndarray_slice_assign_single(self): + for fmt, items, _ in iter_format(5): + for lslice in genslices(5): + for rslice in genslices(5): + for flags in (0, ND_PIL): + + f = flags|ND_WRITABLE + nd = ndarray(items, shape=[5], format=fmt, flags=f) + ex = ndarray(items, shape=[5], format=fmt, flags=f) + mv = memoryview(ex) + + lsterr = None + diff_structure = None + lst = items[:] + try: + lval = lst[lslice] + rval = lst[rslice] + lst[lslice] = lst[rslice] + diff_structure = len(lval) != len(rval) + except Exception as e: + lsterr = e.__class__ + + nderr = None + try: + nd[lslice] = nd[rslice] + except Exception as e: + nderr = e.__class__ + + if diff_structure: # ndarray cannot change shape + self.assertIs(nderr, ValueError) + else: + self.assertEqual(nd.tolist(), lst) + self.assertIs(nderr, lsterr) + + if not is_memoryview_format(fmt): + continue + + mverr = None + try: + mv[lslice] = mv[rslice] + except Exception as e: + mverr = e.__class__ + + if diff_structure: # memoryview cannot change shape + self.assertIs(mverr, ValueError) + else: + self.assertEqual(mv.tolist(), lst) + self.assertEqual(mv, nd) + self.assertIs(mverr, lsterr) + self.verify(mv, obj=ex, + itemsize=nd.itemsize, fmt=fmt, readonly=0, + ndim=nd.ndim, shape=nd.shape, strides=nd.strides, + lst=nd.tolist()) + + def test_ndarray_slice_assign_multidim(self): + shape_t = (2, 3, 5) + ndim = len(shape_t) + nitems = prod(shape_t) + for shape in permutations(shape_t): + + fmt, items, _ = randitems(nitems) + + for flags in (0, ND_PIL): + for _ in range(ITERATIONS): + lslices, rslices = randslice_from_shape(ndim, shape) + + nd = ndarray(items, shape=shape, format=fmt, + flags=flags|ND_WRITABLE) + lst = carray(items, shape) + + listerr = None + try: + result = multislice_assign(lst, lst, lslices, rslices) + except Exception as e: + listerr = e.__class__ + + nderr = None + try: + nd[lslices] = nd[rslices] + except Exception as e: + nderr = e.__class__ + + if nderr or listerr: + self.assertIs(nderr, listerr) + else: + self.assertEqual(nd.tolist(), result) + + def test_ndarray_random(self): + # construction of valid arrays + for _ in range(ITERATIONS): + for fmt in fmtdict['@']: + itemsize = struct.calcsize(fmt) + + t = rand_structure(itemsize, True, maxdim=MAXDIM, + maxshape=MAXSHAPE) + self.assertTrue(verify_structure(*t)) + items = randitems_from_structure(fmt, t) + + x = ndarray_from_structure(items, fmt, t) + xlist = x.tolist() + + mv = memoryview(x) + if is_memoryview_format(fmt): + mvlist = mv.tolist() + self.assertEqual(mvlist, xlist) + + if t[2] > 0: + # ndim > 0: test against suboffsets representation. + y = ndarray_from_structure(items, fmt, t, flags=ND_PIL) + ylist = y.tolist() + self.assertEqual(xlist, ylist) + + mv = memoryview(y) + if is_memoryview_format(fmt): + self.assertEqual(mv, y) + mvlist = mv.tolist() + self.assertEqual(mvlist, ylist) + + if numpy_array: + shape = t[3] + if 0 in shape: + continue # http://projects.scipy.org/numpy/ticket/1910 + z = numpy_array_from_structure(items, fmt, t) + self.verify(x, obj=None, + itemsize=z.itemsize, fmt=fmt, readonly=0, + ndim=z.ndim, shape=z.shape, strides=z.strides, + lst=z.tolist()) + + def test_ndarray_random_invalid(self): + # exceptions during construction of invalid arrays + for _ in range(ITERATIONS): + for fmt in fmtdict['@']: + itemsize = struct.calcsize(fmt) + + t = rand_structure(itemsize, False, maxdim=MAXDIM, + maxshape=MAXSHAPE) + self.assertFalse(verify_structure(*t)) + items = randitems_from_structure(fmt, t) + + nderr = False + try: + x = ndarray_from_structure(items, fmt, t) + except Exception as e: + nderr = e.__class__ + self.assertTrue(nderr) + + if numpy_array: + numpy_err = False + try: + y = numpy_array_from_structure(items, fmt, t) + except Exception as e: + numpy_err = e.__class__ + + if 0: # http://projects.scipy.org/numpy/ticket/1910 + self.assertTrue(numpy_err) + + def test_ndarray_random_slice_assign(self): + # valid slice assignments + for _ in range(ITERATIONS): + for fmt in fmtdict['@']: + itemsize = struct.calcsize(fmt) + + lshape, rshape, lslices, rslices = \ + rand_aligned_slices(maxdim=MAXDIM, maxshape=MAXSHAPE) + tl = rand_structure(itemsize, True, shape=lshape) + tr = rand_structure(itemsize, True, shape=rshape) + self.assertTrue(verify_structure(*tl)) + self.assertTrue(verify_structure(*tr)) + litems = randitems_from_structure(fmt, tl) + ritems = randitems_from_structure(fmt, tr) + + xl = ndarray_from_structure(litems, fmt, tl) + xr = ndarray_from_structure(ritems, fmt, tr) + xl[lslices] = xr[rslices] + xllist = xl.tolist() + xrlist = xr.tolist() + + ml = memoryview(xl) + mr = memoryview(xr) + self.assertEqual(ml.tolist(), xllist) + self.assertEqual(mr.tolist(), xrlist) + + if tl[2] > 0 and tr[2] > 0: + # ndim > 0: test against suboffsets representation. + yl = ndarray_from_structure(litems, fmt, tl, flags=ND_PIL) + yr = ndarray_from_structure(ritems, fmt, tr, flags=ND_PIL) + yl[lslices] = yr[rslices] + yllist = yl.tolist() + yrlist = yr.tolist() + self.assertEqual(xllist, yllist) + self.assertEqual(xrlist, yrlist) + + ml = memoryview(yl) + mr = memoryview(yr) + self.assertEqual(ml.tolist(), yllist) + self.assertEqual(mr.tolist(), yrlist) + + if numpy_array: + if 0 in lshape or 0 in rshape: + continue # http://projects.scipy.org/numpy/ticket/1910 + + zl = numpy_array_from_structure(litems, fmt, tl) + zr = numpy_array_from_structure(ritems, fmt, tr) + zl[lslices] = zr[rslices] + + if not is_overlapping(tl) and not is_overlapping(tr): + # Slice assignment of overlapping structures + # is undefined in NumPy. + self.verify(xl, obj=None, + itemsize=zl.itemsize, fmt=fmt, readonly=0, + ndim=zl.ndim, shape=zl.shape, + strides=zl.strides, lst=zl.tolist()) + + self.verify(xr, obj=None, + itemsize=zr.itemsize, fmt=fmt, readonly=0, + ndim=zr.ndim, shape=zr.shape, + strides=zr.strides, lst=zr.tolist()) + + def test_ndarray_re_export(self): + items = [1,2,3,4,5,6,7,8,9,10,11,12] + + nd = ndarray(items, shape=[3,4], flags=ND_PIL) + ex = ndarray(nd) + + self.assertTrue(ex.flags & ND_PIL) + self.assertIs(ex.obj, nd) + self.assertEqual(ex.suboffsets, (0, -1)) + self.assertFalse(ex.c_contiguous) + self.assertFalse(ex.f_contiguous) + self.assertFalse(ex.contiguous) + + def test_ndarray_zero_shape(self): + # zeros in shape + for flags in (0, ND_PIL): + nd = ndarray([1,2,3], shape=[0], flags=flags) + mv = memoryview(nd) + self.assertEqual(mv, nd) + self.assertEqual(nd.tolist(), []) + self.assertEqual(mv.tolist(), []) + + nd = ndarray([1,2,3], shape=[0,3,3], flags=flags) + self.assertEqual(nd.tolist(), []) + + nd = ndarray([1,2,3], shape=[3,0,3], flags=flags) + self.assertEqual(nd.tolist(), [[], [], []]) + + nd = ndarray([1,2,3], shape=[3,3,0], flags=flags) + self.assertEqual(nd.tolist(), + [[[], [], []], [[], [], []], [[], [], []]]) + + def test_ndarray_zero_strides(self): + # zero strides + for flags in (0, ND_PIL): + nd = ndarray([1], shape=[5], strides=[0], flags=flags) + mv = memoryview(nd) + self.assertEqual(mv, nd) + self.assertEqual(nd.tolist(), [1, 1, 1, 1, 1]) + self.assertEqual(mv.tolist(), [1, 1, 1, 1, 1]) + + def test_ndarray_offset(self): + nd = ndarray(list(range(20)), shape=[3], offset=7) + self.assertEqual(nd.offset, 7) + self.assertEqual(nd.tolist(), [7,8,9]) + + def test_ndarray_memoryview_from_buffer(self): + for flags in (0, ND_PIL): + nd = ndarray(list(range(3)), shape=[3], flags=flags) + m = nd.memoryview_from_buffer() + self.assertEqual(m, nd) + + def test_ndarray_get_pointer(self): + for flags in (0, ND_PIL): + nd = ndarray(list(range(3)), shape=[3], flags=flags) + for i in range(3): + self.assertEqual(nd[i], get_pointer(nd, [i])) + + def test_ndarray_tolist_null_strides(self): + ex = ndarray(list(range(20)), shape=[2,2,5]) + + nd = ndarray(ex, getbuf=PyBUF_ND|PyBUF_FORMAT) + self.assertEqual(nd.tolist(), ex.tolist()) + + m = memoryview(ex) + self.assertEqual(m.tolist(), ex.tolist()) + + def test_ndarray_cmp_contig(self): + + self.assertFalse(cmp_contig(b"123", b"456")) + + x = ndarray(list(range(12)), shape=[3,4]) + y = ndarray(list(range(12)), shape=[4,3]) + self.assertFalse(cmp_contig(x, y)) + + x = ndarray([1], shape=[1], format="B") + self.assertTrue(cmp_contig(x, b'\x01')) + self.assertTrue(cmp_contig(b'\x01', x)) + + def test_ndarray_hash(self): + + a = array.array('L', [1,2,3]) + nd = ndarray(a) + self.assertRaises(ValueError, hash, nd) + + # one-dimensional + b = bytes(list(range(12))) + + nd = ndarray(list(range(12)), shape=[12]) + self.assertEqual(hash(nd), hash(b)) + + # C-contiguous + nd = ndarray(list(range(12)), shape=[3,4]) + self.assertEqual(hash(nd), hash(b)) + + nd = ndarray(list(range(12)), shape=[3,2,2]) + self.assertEqual(hash(nd), hash(b)) + + # Fortran contiguous + b = bytes(transpose(list(range(12)), shape=[4,3])) + nd = ndarray(list(range(12)), shape=[3,4], flags=ND_FORTRAN) + self.assertEqual(hash(nd), hash(b)) + + b = bytes(transpose(list(range(12)), shape=[2,3,2])) + nd = ndarray(list(range(12)), shape=[2,3,2], flags=ND_FORTRAN) + self.assertEqual(hash(nd), hash(b)) + + # suboffsets + b = bytes(list(range(12))) + nd = ndarray(list(range(12)), shape=[2,2,3], flags=ND_PIL) + self.assertEqual(hash(nd), hash(b)) + + # non-byte formats + nd = ndarray(list(range(12)), shape=[2,2,3], format='L') + self.assertEqual(hash(nd), hash(nd.tobytes())) + + def test_memoryview_construction(self): + + items_shape = [(9, []), ([1,2,3], [3]), (list(range(2*3*5)), [2,3,5])] + + # NumPy style, C-contiguous: + for items, shape in items_shape: + + # From PEP-3118 compliant exporter: + ex = ndarray(items, shape=shape) + m = memoryview(ex) + self.assertTrue(m.c_contiguous) + self.assertTrue(m.contiguous) + + ndim = len(shape) + strides = strides_from_shape(ndim, shape, 1, 'C') + lst = carray(items, shape) + + self.verify(m, obj=ex, + itemsize=1, fmt='B', readonly=1, + ndim=ndim, shape=shape, strides=strides, + lst=lst) + + # From memoryview: + m2 = memoryview(m) + self.verify(m2, obj=ex, + itemsize=1, fmt='B', readonly=1, + ndim=ndim, shape=shape, strides=strides, + lst=lst) + + # PyMemoryView_FromBuffer(): no strides + nd = ndarray(ex, getbuf=PyBUF_CONTIG_RO|PyBUF_FORMAT) + self.assertEqual(nd.strides, ()) + m = nd.memoryview_from_buffer() + self.verify(m, obj=None, + itemsize=1, fmt='B', readonly=1, + ndim=ndim, shape=shape, strides=strides, + lst=lst) + + # PyMemoryView_FromBuffer(): no format, shape, strides + nd = ndarray(ex, getbuf=PyBUF_SIMPLE) + self.assertEqual(nd.format, '') + self.assertEqual(nd.shape, ()) + self.assertEqual(nd.strides, ()) + m = nd.memoryview_from_buffer() + + lst = [items] if ndim == 0 else items + self.verify(m, obj=None, + itemsize=1, fmt='B', readonly=1, + ndim=1, shape=[ex.nbytes], strides=(1,), + lst=lst) + + # NumPy style, Fortran contiguous: + for items, shape in items_shape: + + # From PEP-3118 compliant exporter: + ex = ndarray(items, shape=shape, flags=ND_FORTRAN) + m = memoryview(ex) + self.assertTrue(m.f_contiguous) + self.assertTrue(m.contiguous) + + ndim = len(shape) + strides = strides_from_shape(ndim, shape, 1, 'F') + lst = farray(items, shape) + + self.verify(m, obj=ex, + itemsize=1, fmt='B', readonly=1, + ndim=ndim, shape=shape, strides=strides, + lst=lst) + + # From memoryview: + m2 = memoryview(m) + self.verify(m2, obj=ex, + itemsize=1, fmt='B', readonly=1, + ndim=ndim, shape=shape, strides=strides, + lst=lst) + + # PIL style: + for items, shape in items_shape[1:]: + + # From PEP-3118 compliant exporter: + ex = ndarray(items, shape=shape, flags=ND_PIL) + m = memoryview(ex) + + ndim = len(shape) + lst = carray(items, shape) + + self.verify(m, obj=ex, + itemsize=1, fmt='B', readonly=1, + ndim=ndim, shape=shape, strides=ex.strides, + lst=lst) + + # From memoryview: + m2 = memoryview(m) + self.verify(m2, obj=ex, + itemsize=1, fmt='B', readonly=1, + ndim=ndim, shape=shape, strides=ex.strides, + lst=lst) + + # Invalid number of arguments: + self.assertRaises(TypeError, memoryview, b'9', 'x') + # Not a buffer provider: + self.assertRaises(TypeError, memoryview, {}) + # Non-compliant buffer provider: + ex = ndarray([1,2,3], shape=[3]) + nd = ndarray(ex, getbuf=PyBUF_SIMPLE) + self.assertRaises(BufferError, memoryview, nd) + nd = ndarray(ex, getbuf=PyBUF_CONTIG_RO|PyBUF_FORMAT) + self.assertRaises(BufferError, memoryview, nd) + + # ndim > 64 + nd = ndarray([1]*128, shape=[1]*128, format='L') + self.assertRaises(ValueError, memoryview, nd) + self.assertRaises(ValueError, nd.memoryview_from_buffer) + self.assertRaises(ValueError, get_contiguous, nd, PyBUF_READ, 'C') + self.assertRaises(ValueError, get_contiguous, nd, PyBUF_READ, 'F') + self.assertRaises(ValueError, get_contiguous, nd[::-1], PyBUF_READ, 'C') + + def test_memoryview_cast_zero_shape(self): + # Casts are undefined if shape contains zeros. These arrays are + # regarded as C-contiguous by Numpy and PyBuffer_GetContiguous(), + # so they are not caught by the test for C-contiguity in memory_cast(). + items = [1,2,3] + for shape in ([0,3,3], [3,0,3], [0,3,3]): + ex = ndarray(items, shape=shape) + self.assertTrue(ex.c_contiguous) + msrc = memoryview(ex) + self.assertRaises(TypeError, msrc.cast, 'c') + + def test_memoryview_struct_module(self): + + class INT(object): + def __init__(self, val): + self.val = val + def __int__(self): + return self.val + + class IDX(object): + def __init__(self, val): + self.val = val + def __index__(self): + return self.val + + def f(): return 7 + + values = [INT(9), IDX(9), + 2.2+3j, Decimal("-21.1"), 12.2, Fraction(5, 2), + [1,2,3], {4,5,6}, {7:8}, (), (9,), + True, False, None, NotImplemented, + b'a', b'abc', bytearray(b'a'), bytearray(b'abc'), + 'a', 'abc', r'a', r'abc', + f, lambda x: x] + + for fmt, items, item in iter_format(10, 'memoryview'): + ex = ndarray(items, shape=[10], format=fmt, flags=ND_WRITABLE) + nd = ndarray(items, shape=[10], format=fmt, flags=ND_WRITABLE) + m = memoryview(ex) + + struct.pack_into(fmt, nd, 0, item) + m[0] = item + self.assertEqual(m[0], nd[0]) + + itemsize = struct.calcsize(fmt) + if 'P' in fmt: + continue + + for v in values: + struct_err = None + try: + struct.pack_into(fmt, nd, itemsize, v) + except struct.error: + struct_err = struct.error + + mv_err = None + try: + m[1] = v + except (TypeError, ValueError) as e: + mv_err = e.__class__ + + if struct_err or mv_err: + self.assertIsNot(struct_err, None) + self.assertIsNot(mv_err, None) + else: + self.assertEqual(m[1], nd[1]) + + def test_memoryview_cast_zero_strides(self): + # Casts are undefined if strides contains zeros. These arrays are + # (sometimes!) regarded as C-contiguous by Numpy, but not by + # PyBuffer_GetContiguous(). + ex = ndarray([1,2,3], shape=[3], strides=[0]) + self.assertFalse(ex.c_contiguous) + msrc = memoryview(ex) + self.assertRaises(TypeError, msrc.cast, 'c') + + def test_memoryview_cast_invalid(self): + # invalid format + for sfmt in NON_BYTE_FORMAT: + sformat = '@' + sfmt if randrange(2) else sfmt + ssize = struct.calcsize(sformat) + for dfmt in NON_BYTE_FORMAT: + dformat = '@' + dfmt if randrange(2) else dfmt + dsize = struct.calcsize(dformat) + ex = ndarray(list(range(32)), shape=[32//ssize], format=sformat) + msrc = memoryview(ex) + self.assertRaises(TypeError, msrc.cast, dfmt, [32//dsize]) + + for sfmt, sitems, _ in iter_format(1): + ex = ndarray(sitems, shape=[1], format=sfmt) + msrc = memoryview(ex) + for dfmt, _, _ in iter_format(1): + if (not is_memoryview_format(sfmt) or + not is_memoryview_format(dfmt)): + self.assertRaises(ValueError, msrc.cast, dfmt, + [32//dsize]) + else: + if not is_byte_format(sfmt) and not is_byte_format(dfmt): + self.assertRaises(TypeError, msrc.cast, dfmt, + [32//dsize]) + + # invalid shape + size_h = struct.calcsize('h') + size_d = struct.calcsize('d') + ex = ndarray(list(range(2*2*size_d)), shape=[2,2,size_d], format='h') + msrc = memoryview(ex) + self.assertRaises(TypeError, msrc.cast, shape=[2,2,size_h], format='d') + + ex = ndarray(list(range(120)), shape=[1,2,3,4,5]) + m = memoryview(ex) + + # incorrect number of args + self.assertRaises(TypeError, m.cast) + self.assertRaises(TypeError, m.cast, 1, 2, 3) + + # incorrect dest format type + self.assertRaises(TypeError, m.cast, {}) + + # incorrect dest format + self.assertRaises(ValueError, m.cast, "X") + self.assertRaises(ValueError, m.cast, "@X") + self.assertRaises(ValueError, m.cast, "@XY") + + # dest format not implemented + self.assertRaises(ValueError, m.cast, "=B") + self.assertRaises(ValueError, m.cast, "!L") + self.assertRaises(ValueError, m.cast, "l") + self.assertRaises(ValueError, m.cast, "BI") + self.assertRaises(ValueError, m.cast, "xBI") + + # src format not implemented + ex = ndarray([(1,2), (3,4)], shape=[2], format="II") + m = memoryview(ex) + self.assertRaises(NotImplementedError, m.__getitem__, 0) + self.assertRaises(NotImplementedError, m.__setitem__, 0, 8) + self.assertRaises(NotImplementedError, m.tolist) + + # incorrect shape type + ex = ndarray(list(range(120)), shape=[1,2,3,4,5]) + m = memoryview(ex) + self.assertRaises(TypeError, m.cast, "B", shape={}) + + # incorrect shape elements + ex = ndarray(list(range(120)), shape=[2*3*4*5]) + m = memoryview(ex) + self.assertRaises(OverflowError, m.cast, "B", shape=[2**64]) + self.assertRaises(ValueError, m.cast, "B", shape=[-1]) + self.assertRaises(ValueError, m.cast, "B", shape=[2,3,4,5,6,7,-1]) + self.assertRaises(ValueError, m.cast, "B", shape=[2,3,4,5,6,7,0]) + self.assertRaises(TypeError, m.cast, "B", shape=[2,3,4,5,6,7,'x']) + + # N-D -> N-D cast + ex = ndarray(list([9 for _ in range(3*5*7*11)]), shape=[3,5,7,11]) + m = memoryview(ex) + self.assertRaises(TypeError, m.cast, "I", shape=[2,3,4,5]) + + # cast with ndim > 64 + nd = ndarray(list(range(128)), shape=[128], format='I') + m = memoryview(nd) + self.assertRaises(ValueError, m.cast, 'I', [1]*128) + + # view->len not a multiple of itemsize + ex = ndarray(list([9 for _ in range(3*5*7*11)]), shape=[3*5*7*11]) + m = memoryview(ex) + self.assertRaises(TypeError, m.cast, "I", shape=[2,3,4,5]) + + # product(shape) * itemsize != buffer size + ex = ndarray(list([9 for _ in range(3*5*7*11)]), shape=[3*5*7*11]) + m = memoryview(ex) + self.assertRaises(TypeError, m.cast, "B", shape=[2,3,4,5]) + + # product(shape) * itemsize overflow + nd = ndarray(list(range(128)), shape=[128], format='I') + m1 = memoryview(nd) + nd = ndarray(list(range(128)), shape=[128], format='B') + m2 = memoryview(nd) + if sys.maxsize == 2**63-1: + self.assertRaises(TypeError, m1.cast, 'B', + [7, 7, 73, 127, 337, 92737, 649657]) + self.assertRaises(ValueError, m1.cast, 'B', + [2**20, 2**20, 2**10, 2**10, 2**3]) + self.assertRaises(ValueError, m2.cast, 'I', + [2**20, 2**20, 2**10, 2**10, 2**1]) + else: + self.assertRaises(TypeError, m1.cast, 'B', + [1, 2147483647]) + self.assertRaises(ValueError, m1.cast, 'B', + [2**10, 2**10, 2**5, 2**5, 2**1]) + self.assertRaises(ValueError, m2.cast, 'I', + [2**10, 2**10, 2**5, 2**3, 2**1]) + + def test_memoryview_cast(self): + bytespec = ( + ('B', lambda ex: list(ex.tobytes())), + ('b', lambda ex: [x-256 if x > 127 else x for x in list(ex.tobytes())]), + ('c', lambda ex: [bytes(chr(x), 'latin-1') for x in list(ex.tobytes())]), + ) + + def iter_roundtrip(ex, m, items, fmt): + srcsize = struct.calcsize(fmt) + for bytefmt, to_bytelist in bytespec: + + m2 = m.cast(bytefmt) + lst = to_bytelist(ex) + self.verify(m2, obj=ex, + itemsize=1, fmt=bytefmt, readonly=0, + ndim=1, shape=[31*srcsize], strides=(1,), + lst=lst, cast=True) + + m3 = m2.cast(fmt) + self.assertEqual(m3, ex) + lst = ex.tolist() + self.verify(m3, obj=ex, + itemsize=srcsize, fmt=fmt, readonly=0, + ndim=1, shape=[31], strides=(srcsize,), + lst=lst, cast=True) + + # cast from ndim = 0 to ndim = 1 + srcsize = struct.calcsize('I') + ex = ndarray(9, shape=[], format='I') + destitems, destshape = cast_items(ex, 'B', 1) + m = memoryview(ex) + m2 = m.cast('B') + self.verify(m2, obj=ex, + itemsize=1, fmt='B', readonly=1, + ndim=1, shape=destshape, strides=(1,), + lst=destitems, cast=True) + + # cast from ndim = 1 to ndim = 0 + destsize = struct.calcsize('I') + ex = ndarray([9]*destsize, shape=[destsize], format='B') + destitems, destshape = cast_items(ex, 'I', destsize, shape=[]) + m = memoryview(ex) + m2 = m.cast('I', shape=[]) + self.verify(m2, obj=ex, + itemsize=destsize, fmt='I', readonly=1, + ndim=0, shape=(), strides=(), + lst=destitems, cast=True) + + # array.array: roundtrip to/from bytes + for fmt, items, _ in iter_format(31, 'array'): + ex = array.array(fmt, items) + m = memoryview(ex) + iter_roundtrip(ex, m, items, fmt) + + # ndarray: roundtrip to/from bytes + for fmt, items, _ in iter_format(31, 'memoryview'): + ex = ndarray(items, shape=[31], format=fmt, flags=ND_WRITABLE) + m = memoryview(ex) + iter_roundtrip(ex, m, items, fmt) + + def test_memoryview_cast_1D_ND(self): + # Cast between C-contiguous buffers. At least one buffer must + # be 1D, at least one format must be 'c', 'b' or 'B'. + for _tshape in gencastshapes(): + for char in fmtdict['@']: + tfmt = ('', '@')[randrange(2)] + char + tsize = struct.calcsize(tfmt) + n = prod(_tshape) * tsize + obj = 'memoryview' if is_byte_format(tfmt) else 'bytefmt' + for fmt, items, _ in iter_format(n, obj): + size = struct.calcsize(fmt) + shape = [n] if n > 0 else [] + tshape = _tshape + [size] + + ex = ndarray(items, shape=shape, format=fmt) + m = memoryview(ex) + + titems, tshape = cast_items(ex, tfmt, tsize, shape=tshape) + + if titems is None: + self.assertRaises(TypeError, m.cast, tfmt, tshape) + continue + if titems == 'nan': + continue # NaNs in lists are a recipe for trouble. + + # 1D -> ND + nd = ndarray(titems, shape=tshape, format=tfmt) + + m2 = m.cast(tfmt, shape=tshape) + ndim = len(tshape) + strides = nd.strides + lst = nd.tolist() + self.verify(m2, obj=ex, + itemsize=tsize, fmt=tfmt, readonly=1, + ndim=ndim, shape=tshape, strides=strides, + lst=lst, cast=True) + + # ND -> 1D + m3 = m2.cast(fmt) + m4 = m2.cast(fmt, shape=shape) + ndim = len(shape) + strides = ex.strides + lst = ex.tolist() + + self.verify(m3, obj=ex, + itemsize=size, fmt=fmt, readonly=1, + ndim=ndim, shape=shape, strides=strides, + lst=lst, cast=True) + + self.verify(m4, obj=ex, + itemsize=size, fmt=fmt, readonly=1, + ndim=ndim, shape=shape, strides=strides, + lst=lst, cast=True) + + def test_memoryview_tolist(self): + + # Most tolist() tests are in self.verify() etc. + + a = array.array('h', list(range(-6, 6))) + m = memoryview(a) + self.assertEqual(m, a) + self.assertEqual(m.tolist(), a.tolist()) + + a = a[2::3] + m = m[2::3] + self.assertEqual(m, a) + self.assertEqual(m.tolist(), a.tolist()) + + ex = ndarray(list(range(2*3*5*7*11)), shape=[11,2,7,3,5], format='L') + m = memoryview(ex) + self.assertEqual(m.tolist(), ex.tolist()) + + ex = ndarray([(2, 5), (7, 11)], shape=[2], format='lh') + m = memoryview(ex) + self.assertRaises(NotImplementedError, m.tolist) + + ex = ndarray([b'12345'], shape=[1], format="s") + m = memoryview(ex) + self.assertRaises(NotImplementedError, m.tolist) + + ex = ndarray([b"a",b"b",b"c",b"d",b"e",b"f"], shape=[2,3], format='s') + m = memoryview(ex) + self.assertRaises(NotImplementedError, m.tolist) + + def test_memoryview_repr(self): + m = memoryview(bytearray(9)) + r = m.__repr__() + self.assertTrue(r.startswith(" 1: C-contiguous + # different values + nd1 = ndarray(list(range(-15, 15)), shape=[3, 2, 5], format='@h') + nd2 = ndarray(list(range(0, 30)), shape=[3, 2, 5], format='@h') + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + # different shape + nd1 = ndarray(list(range(30)), shape=[2, 3, 5], format='L') + nd2 = ndarray(list(range(30)), shape=[3, 2, 5], format='L') + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + # different format + nd1 = ndarray(list(range(30)), shape=[2, 3, 5], format='L') + nd2 = ndarray(list(range(30)), shape=[2, 3, 5], format='l') + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + ##### ndim > 1: Fortran contiguous + # different values + nd1 = ndarray(list(range(-15, 15)), shape=[5, 2, 3], format='@h', + flags=ND_FORTRAN) + nd2 = ndarray(list(range(0, 30)), shape=[5, 2, 3], format='@h', + flags=ND_FORTRAN) + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + # different shape + nd1 = ndarray(list(range(-15, 15)), shape=[2, 3, 5], format='l', + flags=ND_FORTRAN) + nd2 = ndarray(list(range(-15, 15)), shape=[3, 2, 5], format='l', + flags=ND_FORTRAN) + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + # different format + nd1 = ndarray(list(range(30)), shape=[5, 2, 3], format='@h', + flags=ND_FORTRAN) + nd2 = ndarray(list(range(30)), shape=[5, 2, 3], format='@b', + flags=ND_FORTRAN) + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + ##### ndim > 1: mixed C/Fortran contiguous + lst1 = list(range(-15, 15)) + lst2 = transpose(lst1, [3, 2, 5]) + nd1 = ndarray(lst1, shape=[3, 2, 5], format='@l') + nd2 = ndarray(lst2, shape=[3, 2, 5], format='l', flags=ND_FORTRAN) + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertEqual(v, w) + + ##### ndim > 1: non-contiguous + # different values + ex1 = ndarray(list(range(40)), shape=[5, 8], format='@I') + nd1 = ex1[3:1:-1, ::-2] + ex2 = ndarray(list(range(40)), shape=[5, 8], format='I') + nd2 = ex2[1:3:1, ::-2] + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + # different shape + ex1 = ndarray(list(range(30)), shape=[2, 3, 5], format='b') + nd1 = ex1[1:3:, ::-2] + nd2 = ndarray(list(range(30)), shape=[3, 2, 5], format='b') + nd2 = ex2[1:3:, ::-2] + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + # different format + ex1 = ndarray(list(range(30)), shape=[5, 3, 2], format='i') + nd1 = ex1[1:3:, ::-2] + nd2 = ndarray(list(range(30)), shape=[5, 3, 2], format='@I') + nd2 = ex2[1:3:, ::-2] + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + ##### ndim > 1: zeros in shape + nd1 = ndarray(list(range(30)), shape=[0, 3, 2], format='i') + nd2 = ndarray(list(range(30)), shape=[5, 0, 2], format='@i') + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + # ndim > 1: zero strides + nd1 = ndarray([900]*80, shape=[4, 5, 4], format='@L') + nd2 = ndarray([900], shape=[4, 5, 4], strides=[0, 0, 0], format='L') + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertEqual(v, nd2) + self.assertEqual(w, nd1) + self.assertEqual(v, w) + self.assertEqual(v.tolist(), w.tolist()) + + ##### ndim > 1: suboffsets + ex1 = ndarray(list(range(40)), shape=[5, 8], format='@I') + nd1 = ex1[3:1:-1, ::-2] + ex2 = ndarray(list(range(40)), shape=[5, 8], format='I', flags=ND_PIL) + nd2 = ex2[1:3:1, ::-2] + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + # different shape + ex1 = ndarray(list(range(30)), shape=[2, 3, 5], format='b', flags=ND_PIL) + nd1 = ex1[1:3:, ::-2] + nd2 = ndarray(list(range(30)), shape=[3, 2, 5], format='b') + nd2 = ex2[1:3:, ::-2] + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + # different format + ex1 = ndarray(list(range(30)), shape=[5, 3, 2], format='i', flags=ND_PIL) + nd1 = ex1[1:3:, ::-2] + nd2 = ndarray(list(range(30)), shape=[5, 3, 2], format='@I', flags=ND_PIL) + nd2 = ex2[1:3:, ::-2] + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertNotEqual(v, nd2) + self.assertNotEqual(w, nd1) + self.assertNotEqual(v, w) + + # initialize mixed C/Fortran + suboffsets + lst1 = list(range(-15, 15)) + lst2 = transpose(lst1, [3, 2, 5]) + nd1 = ndarray(lst1, shape=[3, 2, 5], format='@l', flags=ND_PIL) + nd2 = ndarray(lst2, shape=[3, 2, 5], format='l', flags=ND_FORTRAN|ND_PIL) + v = memoryview(nd1) + w = memoryview(nd2) + + self.assertEqual(v, nd1) + self.assertEqual(w, nd2) + self.assertEqual(v, w) + + def test_memoryview_check_released(self): + + a = array.array('d', [1.1, 2.2, 3.3]) + + m = memoryview(a) + m.release() + + # PyMemoryView_FromObject() + self.assertRaises(ValueError, memoryview, m) + # memoryview.cast() + self.assertRaises(ValueError, m.cast, 'c') + # getbuffer() + self.assertRaises(ValueError, ndarray, m) + # memoryview.tolist() + self.assertRaises(ValueError, m.tolist) + # memoryview.tobytes() + self.assertRaises(ValueError, m.tobytes) + # sequence + self.assertRaises(ValueError, eval, "1.0 in m", locals()) + # subscript + self.assertRaises(ValueError, m.__getitem__, 0) + # assignment + self.assertRaises(ValueError, m.__setitem__, 0, 1) + + for attr in ('obj', 'nbytes', 'readonly', 'itemsize', 'format', 'ndim', + 'shape', 'strides', 'suboffsets', 'c_contiguous', + 'f_contiguous', 'contiguous'): + self.assertRaises(ValueError, m.__getattribute__, attr) + + # richcompare + b = array.array('d', [1.1, 2.2, 3.3]) + m1 = memoryview(a) + m2 = memoryview(b) + + self.assertEqual(m1, m2) + m1.release() + self.assertNotEqual(m1, m2) + self.assertNotEqual(m1, a) + self.assertEqual(m1, m1) + + def test_memoryview_tobytes(self): + # Many implicit tests are already in self.verify(). + + nd = ndarray([-529, 576, -625, 676, -729], shape=[5], format='@h') + + m = memoryview(nd) + self.assertEqual(m.tobytes(), nd.tobytes()) + + def test_memoryview_get_contiguous(self): + # Many implicit tests are already in self.verify(). + + # no buffer interface + self.assertRaises(TypeError, get_contiguous, {}, PyBUF_READ, 'F') + + # writable request to read-only object + self.assertRaises(BufferError, get_contiguous, b'x', PyBUF_WRITE, 'C') + + # writable request to non-contiguous object + nd = ndarray([1, 2, 3], shape=[2], strides=[2]) + self.assertRaises(BufferError, get_contiguous, nd, PyBUF_WRITE, 'A') + + # scalar, read-only request from read-only exporter + nd = ndarray(9, shape=(), format="L") + for order in ['C', 'F', 'A']: + m = get_contiguous(nd, PyBUF_READ, order) + self.assertEqual(m, nd) + self.assertEqual(m[()], 9) + + # scalar, read-only request from writable exporter + nd = ndarray(9, shape=(), format="L", flags=ND_WRITABLE) + for order in ['C', 'F', 'A']: + m = get_contiguous(nd, PyBUF_READ, order) + self.assertEqual(m, nd) + self.assertEqual(m[()], 9) + + # scalar, writable request + for order in ['C', 'F', 'A']: + nd[()] = 9 + m = get_contiguous(nd, PyBUF_WRITE, order) + self.assertEqual(m, nd) + self.assertEqual(m[()], 9) + + m[()] = 10 + self.assertEqual(m[()], 10) + self.assertEqual(nd[()], 10) + + # zeros in shape + nd = ndarray([1], shape=[0], format="L", flags=ND_WRITABLE) + for order in ['C', 'F', 'A']: + m = get_contiguous(nd, PyBUF_READ, order) + self.assertRaises(IndexError, m.__getitem__, 0) + self.assertEqual(m, nd) + self.assertEqual(m.tolist(), []) + + nd = ndarray(list(range(8)), shape=[2, 0, 7], format="L", + flags=ND_WRITABLE) + for order in ['C', 'F', 'A']: + m = get_contiguous(nd, PyBUF_READ, order) + self.assertEqual(ndarray(m).tolist(), [[], []]) + + # one-dimensional + nd = ndarray([1], shape=[1], format="h", flags=ND_WRITABLE) + for order in ['C', 'F', 'A']: + m = get_contiguous(nd, PyBUF_WRITE, order) + self.assertEqual(m, nd) + self.assertEqual(m.tolist(), nd.tolist()) + + nd = ndarray([1, 2, 3], shape=[3], format="b", flags=ND_WRITABLE) + for order in ['C', 'F', 'A']: + m = get_contiguous(nd, PyBUF_WRITE, order) + self.assertEqual(m, nd) + self.assertEqual(m.tolist(), nd.tolist()) + + # one-dimensional, non-contiguous + nd = ndarray([1, 2, 3], shape=[2], strides=[2], flags=ND_WRITABLE) + for order in ['C', 'F', 'A']: + m = get_contiguous(nd, PyBUF_READ, order) + self.assertEqual(m, nd) + self.assertEqual(m.tolist(), nd.tolist()) + self.assertRaises(TypeError, m.__setitem__, 1, 20) + self.assertEqual(m[1], 3) + self.assertEqual(nd[1], 3) + + nd = nd[::-1] + for order in ['C', 'F', 'A']: + m = get_contiguous(nd, PyBUF_READ, order) + self.assertEqual(m, nd) + self.assertEqual(m.tolist(), nd.tolist()) + self.assertRaises(TypeError, m.__setitem__, 1, 20) + self.assertEqual(m[1], 1) + self.assertEqual(nd[1], 1) + + # multi-dimensional, contiguous input + nd = ndarray(list(range(12)), shape=[3, 4], flags=ND_WRITABLE) + for order in ['C', 'A']: + m = get_contiguous(nd, PyBUF_WRITE, order) + self.assertEqual(ndarray(m).tolist(), nd.tolist()) + + self.assertRaises(BufferError, get_contiguous, nd, PyBUF_WRITE, 'F') + m = get_contiguous(nd, PyBUF_READ, order) + self.assertEqual(ndarray(m).tolist(), nd.tolist()) + + nd = ndarray(list(range(12)), shape=[3, 4], + flags=ND_WRITABLE|ND_FORTRAN) + for order in ['F', 'A']: + m = get_contiguous(nd, PyBUF_WRITE, order) + self.assertEqual(ndarray(m).tolist(), nd.tolist()) + + self.assertRaises(BufferError, get_contiguous, nd, PyBUF_WRITE, 'C') + m = get_contiguous(nd, PyBUF_READ, order) + self.assertEqual(ndarray(m).tolist(), nd.tolist()) + + # multi-dimensional, non-contiguous input + nd = ndarray(list(range(12)), shape=[3, 4], flags=ND_WRITABLE|ND_PIL) + for order in ['C', 'F', 'A']: + self.assertRaises(BufferError, get_contiguous, nd, PyBUF_WRITE, + order) + m = get_contiguous(nd, PyBUF_READ, order) + self.assertEqual(ndarray(m).tolist(), nd.tolist()) + + # flags + nd = ndarray([1,2,3,4,5], shape=[3], strides=[2]) + m = get_contiguous(nd, PyBUF_READ, 'C') + self.assertTrue(m.c_contiguous) + + def test_memoryview_serializing(self): + + # C-contiguous + size = struct.calcsize('i') + a = array.array('i', [1,2,3,4,5]) + m = memoryview(a) + buf = io.BytesIO(m) + b = bytearray(5*size) + buf.readinto(b) + self.assertEqual(m.tobytes(), b) + + # C-contiguous, multi-dimensional + size = struct.calcsize('L') + nd = ndarray(list(range(12)), shape=[2,3,2], format="L") + m = memoryview(nd) + buf = io.BytesIO(m) + b = bytearray(2*3*2*size) + buf.readinto(b) + self.assertEqual(m.tobytes(), b) + + # Fortran contiguous, multi-dimensional + #size = struct.calcsize('L') + #nd = ndarray(list(range(12)), shape=[2,3,2], format="L", + # flags=ND_FORTRAN) + #m = memoryview(nd) + #buf = io.BytesIO(m) + #b = bytearray(2*3*2*size) + #buf.readinto(b) + #self.assertEqual(m.tobytes(), b) + + def test_memoryview_hash(self): + + # bytes exporter + b = bytes(list(range(12))) + m = memoryview(b) + self.assertEqual(hash(b), hash(m)) + + # C-contiguous + mc = m.cast('c', shape=[3,4]) + self.assertEqual(hash(mc), hash(b)) + + # non-contiguous + mx = m[::-2] + b = bytes(list(range(12))[::-2]) + self.assertEqual(hash(mx), hash(b)) + + # Fortran contiguous + nd = ndarray(list(range(30)), shape=[3,2,5], flags=ND_FORTRAN) + m = memoryview(nd) + self.assertEqual(hash(m), hash(nd)) + + # multi-dimensional slice + nd = ndarray(list(range(30)), shape=[3,2,5]) + x = nd[::2, ::, ::-1] + m = memoryview(x) + self.assertEqual(hash(m), hash(x)) + + # multi-dimensional slice with suboffsets + nd = ndarray(list(range(30)), shape=[2,5,3], flags=ND_PIL) + x = nd[::2, ::, ::-1] + m = memoryview(x) + self.assertEqual(hash(m), hash(x)) + + # non-byte formats + nd = ndarray(list(range(12)), shape=[2,2,3], format='L') + m = memoryview(nd) + self.assertEqual(hash(m), hash(nd.tobytes())) + + nd = ndarray(list(range(-6, 6)), shape=[2,2,3], format='h') + m = memoryview(nd) + self.assertEqual(hash(m), hash(nd.tobytes())) + + def test_memoryview_release(self): + + # Create re-exporter from getbuffer(memoryview), then release the view. + a = bytearray([1,2,3]) + m = memoryview(a) + nd = ndarray(m) # re-exporter + self.assertRaises(BufferError, m.release) + del nd + m.release() + + # chained views + a = bytearray([1,2,3]) + m1 = memoryview(a) + m2 = memoryview(m1) + nd = ndarray(m2) # re-exporter + m1.release() + self.assertRaises(BufferError, m2.release) + del nd + m2.release() + + # Allow changing layout while buffers are exported. + nd = ndarray([1,2,3], shape=[3], flags=ND_VAREXPORT) + m1 = memoryview(nd) + + nd.push([4,5,6,7,8], shape=[5]) # mutate nd + m2 = memoryview(nd) + + x = memoryview(m1) + self.assertEqual(x.tolist(), m1.tolist()) + + y = memoryview(m2) + self.assertEqual(y.tolist(), m2.tolist()) + self.assertEqual(y.tolist(), nd.tolist()) + m2.release() + y.release() + + nd.pop() # pop the current view + self.assertEqual(x.tolist(), nd.tolist()) + + del nd + m1.release() + x.release() + + # If multiple memoryviews share the same managed buffer, implicit + # release() in the context manager's __exit__() method should still + # work. + def catch22(b): + with memoryview(b) as m2: + pass + + x = bytearray(b'123') + with memoryview(x) as m1: + catch22(m1) + self.assertEqual(m1[0], ord(b'1')) + + # XXX If m1 has exports, raise BufferError. + # x = bytearray(b'123') + # with memoryview(x) as m1: + # ex = ndarray(m1) + # m1[0] == ord(b'1') + + def test_issue_7385(self): + x = ndarray([1,2,3], shape=[3], flags=ND_GETBUF_FAIL) + self.assertRaises(BufferError, memoryview, x) + + +def test_main(): + support.run_unittest(TestBufferProtocol) + + +if __name__ == "__main__": + test_main() diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index a5a0ca1b83d..8809930acbe 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -24,15 +24,14 @@ class AbstractMemoryTests: return filter(None, [self.ro_type, self.rw_type]) def check_getitem_with_type(self, tp): - item = self.getitem_type b = tp(self._source) oldrefcount = sys.getrefcount(b) m = self._view(b) - self.assertEqual(m[0], item(b"a")) - self.assertIsInstance(m[0], bytes) - self.assertEqual(m[5], item(b"f")) - self.assertEqual(m[-1], item(b"f")) - self.assertEqual(m[-6], item(b"a")) + self.assertEqual(m[0], ord(b"a")) + self.assertIsInstance(m[0], int) + self.assertEqual(m[5], ord(b"f")) + self.assertEqual(m[-1], ord(b"f")) + self.assertEqual(m[-6], ord(b"a")) # Bounds checking self.assertRaises(IndexError, lambda: m[6]) self.assertRaises(IndexError, lambda: m[-7]) @@ -76,7 +75,9 @@ class AbstractMemoryTests: b = self.rw_type(self._source) oldrefcount = sys.getrefcount(b) m = self._view(b) - m[0] = tp(b"0") + m[0] = ord(b'1') + self._check_contents(tp, b, b"1bcdef") + m[0:1] = tp(b"0") self._check_contents(tp, b, b"0bcdef") m[1:3] = tp(b"12") self._check_contents(tp, b, b"012def") @@ -102,10 +103,17 @@ class AbstractMemoryTests: # Wrong index/slice types self.assertRaises(TypeError, setitem, 0.0, b"a") self.assertRaises(TypeError, setitem, (0,), b"a") + self.assertRaises(TypeError, setitem, (slice(0,1,1), 0), b"a") + self.assertRaises(TypeError, setitem, (0, slice(0,1,1)), b"a") + self.assertRaises(TypeError, setitem, (0,), b"a") self.assertRaises(TypeError, setitem, "a", b"a") + # Not implemented: multidimensional slices + slices = (slice(0,1,1), slice(0,1,2)) + self.assertRaises(NotImplementedError, setitem, slices, b"a") # Trying to resize the memory object - self.assertRaises(ValueError, setitem, 0, b"") - self.assertRaises(ValueError, setitem, 0, b"ab") + exc = ValueError if m.format == 'c' else TypeError + self.assertRaises(exc, setitem, 0, b"") + self.assertRaises(exc, setitem, 0, b"ab") self.assertRaises(ValueError, setitem, slice(1,1), b"a") self.assertRaises(ValueError, setitem, slice(0,2), b"a") @@ -175,7 +183,7 @@ class AbstractMemoryTests: self.assertEqual(m.shape, (6,)) self.assertEqual(len(m), 6) self.assertEqual(m.strides, (self.itemsize,)) - self.assertEqual(m.suboffsets, None) + self.assertEqual(m.suboffsets, ()) return m def test_attributes_readonly(self): @@ -209,12 +217,16 @@ class AbstractMemoryTests: # If tp is a factory rather than a plain type, skip continue + class MyView(): + def __init__(self, base): + self.m = memoryview(base) class MySource(tp): pass class MyObject: pass - # Create a reference cycle through a memoryview object + # Create a reference cycle through a memoryview object. + # This exercises mbuf_clear(). b = MySource(tp(b'abc')) m = self._view(b) o = MyObject() @@ -226,6 +238,17 @@ class AbstractMemoryTests: gc.collect() self.assertTrue(wr() is None, wr()) + # This exercises memory_clear(). + m = MyView(tp(b'abc')) + o = MyObject() + m.x = m + m.o = o + wr = weakref.ref(o) + m = o = None + # The cycle must be broken + gc.collect() + self.assertTrue(wr() is None, wr()) + def _check_released(self, m, tp): check = self.assertRaisesRegex(ValueError, "released") with check: bytes(m) @@ -283,9 +306,12 @@ class AbstractMemoryTests: i = io.BytesIO(b'ZZZZ') self.assertRaises(TypeError, i.readinto, m) + def test_getbuf_fail(self): + self.assertRaises(TypeError, self._view, {}) + def test_hash(self): # Memoryviews of readonly (hashable) types are hashable, and they - # hash as the corresponding object. + # hash as hash(obj.tobytes()). tp = self.ro_type if tp is None: self.skipTest("no read-only type to test") diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index bf22df2269f..551c3a5a4a5 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -773,8 +773,8 @@ class SizeofTest(unittest.TestCase): check(int(PyLong_BASE), size(vh) + 2*self.longdigit) check(int(PyLong_BASE**2-1), size(vh) + 2*self.longdigit) check(int(PyLong_BASE**2), size(vh) + 3*self.longdigit) - # memory (Py_buffer + hash value) - check(memoryview(b''), size(h + 'PP2P2i7P' + 'P')) + # memoryview + check(memoryview(b''), size(h + 'PPiP4P2i5P3cP')) # module check(unittest, size(h + '3P')) # None diff --git a/Misc/ACKS b/Misc/ACKS index 2e37b419a1f..48ef08061ee 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1041,6 +1041,7 @@ John Viega Kannan Vijayan Kurt Vile Norman Vine +Pauli Virtanen Frank Visser Johannes Vogel Sjoerd de Vries diff --git a/Misc/NEWS b/Misc/NEWS index 047f272386b..3d5a7113051 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -10,6 +10,23 @@ What's New in Python 3.3 Alpha 1? Core and Builtins ----------------- +- Issue #10181: New memoryview implementation fixes multiple ownership + and lifetime issues of dynamically allocated Py_buffer members (#9990) + as well as crashes (#8305, #7433). Many new features have been added + (See whatsnew/3.3), and the documentation has been updated extensively. + The ndarray test object from _testbuffer.c implements all aspects of + PEP-3118, so further development towards the complete implementation + of the PEP can proceed in a test-driven manner. + + Thanks to Nick Coghlan, Antoine Pitrou and Pauli Virtanen for review + and many ideas. + +- Issue #12834: Fix incorrect results of memoryview.tobytes() for + non-contiguous arrays. + +- Issue #5231: Introduce memoryview.cast() method that allows changing + format and shape without making a copy of the underlying memory. + - Issue #14084: Fix a file descriptor leak when importing a module with a bad encoding. diff --git a/Misc/valgrind-python.supp b/Misc/valgrind-python.supp index 194ecbf2b94..20dbf1e7b8d 100644 --- a/Misc/valgrind-python.supp +++ b/Misc/valgrind-python.supp @@ -412,4 +412,15 @@ fun:SHA1_Update } +{ + test_buffer_non_debug + Memcheck:Addr4 + fun:PyUnicodeUCS2_FSConverter +} + +{ + test_buffer_non_debug + Memcheck:Addr4 + fun:PyUnicode_FSConverter +} diff --git a/Modules/_testbuffer.c b/Modules/_testbuffer.c new file mode 100644 index 00000000000..39a7bcc9f05 --- /dev/null +++ b/Modules/_testbuffer.c @@ -0,0 +1,2683 @@ +/* C Extension module to test all aspects of PEP-3118. + Written by Stefan Krah. */ + + +#define PY_SSIZE_T_CLEAN + +#include "Python.h" + + +/* struct module */ +PyObject *structmodule = NULL; +PyObject *Struct = NULL; +PyObject *calcsize = NULL; + +/* cache simple format string */ +static const char *simple_fmt = "B"; +PyObject *simple_format = NULL; +#define SIMPLE_FORMAT(fmt) (fmt == NULL || strcmp(fmt, "B") == 0) + + +/**************************************************************************/ +/* NDArray Object */ +/**************************************************************************/ + +static PyTypeObject NDArray_Type; +#define NDArray_Check(v) (Py_TYPE(v) == &NDArray_Type) + +#define CHECK_LIST_OR_TUPLE(v) \ + if (!PyList_Check(v) && !PyTuple_Check(v)) { \ + PyErr_SetString(PyExc_TypeError, \ + #v " must be a list or a tuple"); \ + return NULL; \ + } \ + +#define PyMem_XFree(v) \ + do { if (v) PyMem_Free(v); } while (0) + +/* Maximum number of dimensions. */ +#define ND_MAX_NDIM (2 * PyBUF_MAX_NDIM) + +/* Check for the presence of suboffsets in the first dimension. */ +#define HAVE_PTR(suboffsets) (suboffsets && suboffsets[0] >= 0) +/* Adjust ptr if suboffsets are present. */ +#define ADJUST_PTR(ptr, suboffsets) \ + (HAVE_PTR(suboffsets) ? *((char**)ptr) + suboffsets[0] : ptr) + +/* User configurable flags for the ndarray */ +#define ND_VAREXPORT 0x001 /* change layout while buffers are exported */ + +/* User configurable flags for each base buffer */ +#define ND_WRITABLE 0x002 /* mark base buffer as writable */ +#define ND_FORTRAN 0x004 /* Fortran contiguous layout */ +#define ND_SCALAR 0x008 /* scalar: ndim = 0 */ +#define ND_PIL 0x010 /* convert to PIL-style array (suboffsets) */ +#define ND_GETBUF_FAIL 0x020 /* test issue 7385 */ + +/* Default: NumPy style (strides), read-only, no var-export, C-style layout */ +#define ND_DEFAULT 0x0 + +/* Internal flags for the base buffer */ +#define ND_C 0x040 /* C contiguous layout (default) */ +#define ND_OWN_ARRAYS 0x080 /* consumer owns arrays */ +#define ND_UNUSED 0x100 /* initializer */ + +/* ndarray properties */ +#define ND_IS_CONSUMER(nd) \ + (((NDArrayObject *)nd)->head == &((NDArrayObject *)nd)->staticbuf) + +/* ndbuf->flags properties */ +#define ND_C_CONTIGUOUS(flags) (!!(flags&(ND_SCALAR|ND_C))) +#define ND_FORTRAN_CONTIGUOUS(flags) (!!(flags&(ND_SCALAR|ND_FORTRAN))) +#define ND_ANY_CONTIGUOUS(flags) (!!(flags&(ND_SCALAR|ND_C|ND_FORTRAN))) + +/* getbuffer() requests */ +#define REQ_INDIRECT(flags) ((flags&PyBUF_INDIRECT) == PyBUF_INDIRECT) +#define REQ_C_CONTIGUOUS(flags) ((flags&PyBUF_C_CONTIGUOUS) == PyBUF_C_CONTIGUOUS) +#define REQ_F_CONTIGUOUS(flags) ((flags&PyBUF_F_CONTIGUOUS) == PyBUF_F_CONTIGUOUS) +#define REQ_ANY_CONTIGUOUS(flags) ((flags&PyBUF_ANY_CONTIGUOUS) == PyBUF_ANY_CONTIGUOUS) +#define REQ_STRIDES(flags) ((flags&PyBUF_STRIDES) == PyBUF_STRIDES) +#define REQ_SHAPE(flags) ((flags&PyBUF_ND) == PyBUF_ND) +#define REQ_WRITABLE(flags) (flags&PyBUF_WRITABLE) +#define REQ_FORMAT(flags) (flags&PyBUF_FORMAT) + + +/* Single node of a list of base buffers. The list is needed to implement + changes in memory layout while exported buffers are active. */ +static PyTypeObject NDArray_Type; + +struct ndbuf; +typedef struct ndbuf { + struct ndbuf *next; + struct ndbuf *prev; + Py_ssize_t len; /* length of data */ + Py_ssize_t offset; /* start of the array relative to data */ + char *data; /* raw data */ + int flags; /* capabilities of the base buffer */ + Py_ssize_t exports; /* number of exports */ + Py_buffer base; /* base buffer */ +} ndbuf_t; + +typedef struct { + PyObject_HEAD + int flags; /* ndarray flags */ + ndbuf_t staticbuf; /* static buffer for re-exporting mode */ + ndbuf_t *head; /* currently active base buffer */ +} NDArrayObject; + + +static ndbuf_t * +ndbuf_new(Py_ssize_t nitems, Py_ssize_t itemsize, Py_ssize_t offset, int flags) +{ + ndbuf_t *ndbuf; + Py_buffer *base; + Py_ssize_t len; + + len = nitems * itemsize; + if (offset % itemsize) { + PyErr_SetString(PyExc_ValueError, + "offset must be a multiple of itemsize"); + return NULL; + } + if (offset < 0 || offset+itemsize > len) { + PyErr_SetString(PyExc_ValueError, "offset out of bounds"); + return NULL; + } + + ndbuf = PyMem_Malloc(sizeof *ndbuf); + if (ndbuf == NULL) { + PyErr_NoMemory(); + return NULL; + } + + ndbuf->next = NULL; + ndbuf->prev = NULL; + ndbuf->len = len; + ndbuf->offset= offset; + + ndbuf->data = PyMem_Malloc(len); + if (ndbuf->data == NULL) { + PyErr_NoMemory(); + PyMem_Free(ndbuf); + return NULL; + } + + ndbuf->flags = flags; + ndbuf->exports = 0; + + base = &ndbuf->base; + base->obj = NULL; + base->buf = ndbuf->data; + base->len = len; + base->itemsize = 1; + base->readonly = 0; + base->format = NULL; + base->ndim = 1; + base->shape = NULL; + base->strides = NULL; + base->suboffsets = NULL; + base->internal = ndbuf; + + return ndbuf; +} + +static void +ndbuf_free(ndbuf_t *ndbuf) +{ + Py_buffer *base = &ndbuf->base; + + PyMem_XFree(ndbuf->data); + PyMem_XFree(base->format); + PyMem_XFree(base->shape); + PyMem_XFree(base->strides); + PyMem_XFree(base->suboffsets); + + PyMem_Free(ndbuf); +} + +static void +ndbuf_push(NDArrayObject *nd, ndbuf_t *elt) +{ + elt->next = nd->head; + if (nd->head) nd->head->prev = elt; + nd->head = elt; + elt->prev = NULL; +} + +static void +ndbuf_delete(NDArrayObject *nd, ndbuf_t *elt) +{ + if (elt->prev) + elt->prev->next = elt->next; + else + nd->head = elt->next; + + if (elt->next) + elt->next->prev = elt->prev; + + ndbuf_free(elt); +} + +static void +ndbuf_pop(NDArrayObject *nd) +{ + ndbuf_delete(nd, nd->head); +} + + +static PyObject * +ndarray_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + NDArrayObject *nd; + + nd = PyObject_New(NDArrayObject, &NDArray_Type); + if (nd == NULL) + return NULL; + + nd->flags = 0; + nd->head = NULL; + return (PyObject *)nd; +} + +static void +ndarray_dealloc(NDArrayObject *self) +{ + if (self->head) { + if (ND_IS_CONSUMER(self)) { + Py_buffer *base = &self->head->base; + if (self->head->flags & ND_OWN_ARRAYS) { + PyMem_XFree(base->shape); + PyMem_XFree(base->strides); + PyMem_XFree(base->suboffsets); + } + PyBuffer_Release(base); + } + else { + while (self->head) + ndbuf_pop(self); + } + } + PyObject_Del(self); +} + +static int +ndarray_init_staticbuf(PyObject *exporter, NDArrayObject *nd, int flags) +{ + Py_buffer *base = &nd->staticbuf.base; + + if (PyObject_GetBuffer(exporter, base, flags) < 0) + return -1; + + nd->head = &nd->staticbuf; + + nd->head->next = NULL; + nd->head->prev = NULL; + nd->head->len = -1; + nd->head->offset = -1; + nd->head->data = NULL; + + nd->head->flags = base->readonly ? 0 : ND_WRITABLE; + nd->head->exports = 0; + + return 0; +} + +static void +init_flags(ndbuf_t *ndbuf) +{ + if (ndbuf->base.ndim == 0) + ndbuf->flags |= ND_SCALAR; + if (ndbuf->base.suboffsets) + ndbuf->flags |= ND_PIL; + if (PyBuffer_IsContiguous(&ndbuf->base, 'C')) + ndbuf->flags |= ND_C; + if (PyBuffer_IsContiguous(&ndbuf->base, 'F')) + ndbuf->flags |= ND_FORTRAN; +} + + +/****************************************************************************/ +/* Buffer/List conversions */ +/****************************************************************************/ + +static Py_ssize_t *strides_from_shape(const ndbuf_t *, int flags); + +/* Get number of members in a struct: see issue #12740 */ +typedef struct { + PyObject_HEAD + Py_ssize_t s_size; + Py_ssize_t s_len; +} PyPartialStructObject; + +static Py_ssize_t +get_nmemb(PyObject *s) +{ + return ((PyPartialStructObject *)s)->s_len; +} + +/* Pack all items into the buffer of 'obj'. The 'format' parameter must be + in struct module syntax. For standard C types, a single item is an integer. + For compound types, a single item is a tuple of integers. */ +static int +pack_from_list(PyObject *obj, PyObject *items, PyObject *format, + Py_ssize_t itemsize) +{ + PyObject *structobj, *pack_into; + PyObject *args, *offset; + PyObject *item, *tmp; + Py_ssize_t nitems; /* number of items */ + Py_ssize_t nmemb; /* number of members in a single item */ + Py_ssize_t i, j; + int ret = 0; + + assert(PyObject_CheckBuffer(obj)); + assert(PyList_Check(items) || PyTuple_Check(items)); + + structobj = PyObject_CallFunctionObjArgs(Struct, format, NULL); + if (structobj == NULL) + return -1; + + nitems = PySequence_Fast_GET_SIZE(items); + nmemb = get_nmemb(structobj); + assert(nmemb >= 1); + + pack_into = PyObject_GetAttrString(structobj, "pack_into"); + if (pack_into == NULL) { + Py_DECREF(structobj); + return -1; + } + + /* nmemb >= 1 */ + args = PyTuple_New(2 + nmemb); + if (args == NULL) { + Py_DECREF(pack_into); + Py_DECREF(structobj); + return -1; + } + + offset = NULL; + for (i = 0; i < nitems; i++) { + /* Loop invariant: args[j] are borrowed references or NULL. */ + PyTuple_SET_ITEM(args, 0, obj); + for (j = 1; j < 2+nmemb; j++) + PyTuple_SET_ITEM(args, j, NULL); + + Py_XDECREF(offset); + offset = PyLong_FromSsize_t(i*itemsize); + if (offset == NULL) { + ret = -1; + break; + } + PyTuple_SET_ITEM(args, 1, offset); + + item = PySequence_Fast_GET_ITEM(items, i); + if ((PyBytes_Check(item) || PyLong_Check(item) || + PyFloat_Check(item)) && nmemb == 1) { + PyTuple_SET_ITEM(args, 2, item); + } + else if ((PyList_Check(item) || PyTuple_Check(item)) && + PySequence_Length(item) == nmemb) { + for (j = 0; j < nmemb; j++) { + tmp = PySequence_Fast_GET_ITEM(item, j); + PyTuple_SET_ITEM(args, 2+j, tmp); + } + } + else { + PyErr_SetString(PyExc_ValueError, + "mismatch between initializer element and format string"); + ret = -1; + break; + } + + tmp = PyObject_CallObject(pack_into, args); + if (tmp == NULL) { + ret = -1; + break; + } + Py_DECREF(tmp); + } + + Py_INCREF(obj); /* args[0] */ + /* args[1]: offset is either NULL or should be dealloc'd */ + for (i = 2; i < 2+nmemb; i++) { + tmp = PyTuple_GET_ITEM(args, i); + Py_XINCREF(tmp); + } + Py_DECREF(args); + + Py_DECREF(pack_into); + Py_DECREF(structobj); + return ret; + +} + +/* Pack single element */ +static int +pack_single(char *ptr, PyObject *item, const char *fmt, Py_ssize_t itemsize) +{ + PyObject *structobj = NULL, *pack_into = NULL, *args = NULL; + PyObject *format = NULL, *mview = NULL, *zero = NULL; + Py_ssize_t i, nmemb; + int ret = -1; + PyObject *x; + + if (fmt == NULL) fmt = "B"; + + format = PyUnicode_FromString(fmt); + if (format == NULL) + goto out; + + structobj = PyObject_CallFunctionObjArgs(Struct, format, NULL); + if (structobj == NULL) + goto out; + + nmemb = get_nmemb(structobj); + assert(nmemb >= 1); + + mview = PyMemoryView_FromMemory(ptr, itemsize, PyBUF_WRITE); + if (mview == NULL) + goto out; + + zero = PyLong_FromLong(0); + if (zero == NULL) + goto out; + + pack_into = PyObject_GetAttrString(structobj, "pack_into"); + if (pack_into == NULL) + goto out; + + args = PyTuple_New(2+nmemb); + if (args == NULL) + goto out; + + PyTuple_SET_ITEM(args, 0, mview); + PyTuple_SET_ITEM(args, 1, zero); + + if ((PyBytes_Check(item) || PyLong_Check(item) || + PyFloat_Check(item)) && nmemb == 1) { + PyTuple_SET_ITEM(args, 2, item); + } + else if ((PyList_Check(item) || PyTuple_Check(item)) && + PySequence_Length(item) == nmemb) { + for (i = 0; i < nmemb; i++) { + x = PySequence_Fast_GET_ITEM(item, i); + PyTuple_SET_ITEM(args, 2+i, x); + } + } + else { + PyErr_SetString(PyExc_ValueError, + "mismatch between initializer element and format string"); + goto args_out; + } + + x = PyObject_CallObject(pack_into, args); + if (x != NULL) { + Py_DECREF(x); + ret = 0; + } + + +args_out: + for (i = 0; i < 2+nmemb; i++) + Py_XINCREF(PyTuple_GET_ITEM(args, i)); + Py_XDECREF(args); +out: + Py_XDECREF(pack_into); + Py_XDECREF(zero); + Py_XDECREF(mview); + Py_XDECREF(structobj); + Py_XDECREF(format); + return ret; +} + +static void +copy_rec(const Py_ssize_t *shape, Py_ssize_t ndim, Py_ssize_t itemsize, + char *dptr, const Py_ssize_t *dstrides, const Py_ssize_t *dsuboffsets, + char *sptr, const Py_ssize_t *sstrides, const Py_ssize_t *ssuboffsets, + char *mem) +{ + Py_ssize_t i; + + assert(ndim >= 1); + + if (ndim == 1) { + if (!HAVE_PTR(dsuboffsets) && !HAVE_PTR(ssuboffsets) && + dstrides[0] == itemsize && sstrides[0] == itemsize) { + memmove(dptr, sptr, shape[0] * itemsize); + } + else { + char *p; + assert(mem != NULL); + for (i=0, p=mem; iformat == NULL && src->format == NULL) || \ + (strcmp(dest->format, src->format) == 0)); + + if (!same_fmt || + dest->itemsize != src->itemsize || + dest->ndim != src->ndim) + return -1; + + for (i = 0; i < dest->ndim; i++) { + if (dest->shape[i] != src->shape[i]) + return -1; + if (dest->shape[i] == 0) + break; + } + + return 0; +} + +/* Copy src to dest. Both buffers must have the same format, itemsize, + ndim and shape. Copying is atomic, the function never fails with + a partial copy. */ +static int +copy_buffer(Py_buffer *dest, Py_buffer *src) +{ + char *mem = NULL; + + assert(dest->ndim > 0); + + if (cmp_structure(dest, src) < 0) { + PyErr_SetString(PyExc_ValueError, + "ndarray assignment: lvalue and rvalue have different structures"); + return -1; + } + + if ((dest->suboffsets && dest->suboffsets[dest->ndim-1] >= 0) || + (src->suboffsets && src->suboffsets[src->ndim-1] >= 0) || + dest->strides[dest->ndim-1] != dest->itemsize || + src->strides[src->ndim-1] != src->itemsize) { + mem = PyMem_Malloc(dest->shape[dest->ndim-1] * dest->itemsize); + if (mem == NULL) { + PyErr_NoMemory(); + return -1; + } + } + + copy_rec(dest->shape, dest->ndim, dest->itemsize, + dest->buf, dest->strides, dest->suboffsets, + src->buf, src->strides, src->suboffsets, + mem); + + PyMem_XFree(mem); + return 0; +} + + +/* Unpack single element */ +static PyObject * +unpack_single(char *ptr, const char *fmt, Py_ssize_t itemsize) +{ + PyObject *x, *unpack_from, *mview; + + if (fmt == NULL) { + fmt = "B"; + itemsize = 1; + } + + unpack_from = PyObject_GetAttrString(structmodule, "unpack_from"); + if (unpack_from == NULL) + return NULL; + + mview = PyMemoryView_FromMemory(ptr, itemsize, PyBUF_READ); + if (mview == NULL) { + Py_DECREF(unpack_from); + return NULL; + } + + x = PyObject_CallFunction(unpack_from, "sO", fmt, mview); + Py_DECREF(unpack_from); + Py_DECREF(mview); + if (x == NULL) + return NULL; + + if (PyTuple_GET_SIZE(x) == 1) { + PyObject *tmp = PyTuple_GET_ITEM(x, 0); + Py_INCREF(tmp); + Py_DECREF(x); + return tmp; + } + + return x; +} + +/* Unpack a multi-dimensional matrix into a nested list. Return a scalar + for ndim = 0. */ +static PyObject * +unpack_rec(PyObject *unpack_from, char *ptr, PyObject *mview, char *item, + const Py_ssize_t *shape, const Py_ssize_t *strides, + const Py_ssize_t *suboffsets, Py_ssize_t ndim, Py_ssize_t itemsize) +{ + PyObject *lst, *x; + Py_ssize_t i; + + assert(ndim >= 0); + assert(shape != NULL); + assert(strides != NULL); + + if (ndim == 0) { + memcpy(item, ptr, itemsize); + x = PyObject_CallFunctionObjArgs(unpack_from, mview, NULL); + if (x == NULL) + return NULL; + if (PyTuple_GET_SIZE(x) == 1) { + PyObject *tmp = PyTuple_GET_ITEM(x, 0); + Py_INCREF(tmp); + Py_DECREF(x); + return tmp; + } + return x; + } + + lst = PyList_New(shape[0]); + if (lst == NULL) + return NULL; + + for (i = 0; i < shape[0]; ptr+=strides[0], i++) { + char *nextptr = ADJUST_PTR(ptr, suboffsets); + + x = unpack_rec(unpack_from, nextptr, mview, item, + shape+1, strides+1, suboffsets ? suboffsets+1 : NULL, + ndim-1, itemsize); + if (x == NULL) { + Py_DECREF(lst); + return NULL; + } + + PyList_SET_ITEM(lst, i, x); + } + + return lst; +} + + +static PyObject * +ndarray_as_list(NDArrayObject *nd) +{ + PyObject *structobj = NULL, *unpack_from = NULL; + PyObject *lst = NULL, *mview = NULL; + Py_buffer *base = &nd->head->base; + Py_ssize_t *shape = base->shape; + Py_ssize_t *strides = base->strides; + Py_ssize_t simple_shape[1]; + Py_ssize_t simple_strides[1]; + char *item = NULL; + PyObject *format; + char *fmt = base->format; + + base = &nd->head->base; + + if (fmt == NULL) { + PyErr_SetString(PyExc_ValueError, + "ndarray: tolist() does not support format=NULL, use " + "tobytes()"); + return NULL; + } + if (shape == NULL) { + assert(ND_C_CONTIGUOUS(nd->head->flags)); + assert(base->strides == NULL); + assert(base->ndim <= 1); + shape = simple_shape; + shape[0] = base->len; + strides = simple_strides; + strides[0] = base->itemsize; + } + else if (strides == NULL) { + assert(ND_C_CONTIGUOUS(nd->head->flags)); + strides = strides_from_shape(nd->head, 0); + if (strides == NULL) + return NULL; + } + + format = PyUnicode_FromString(fmt); + if (format == NULL) + goto out; + + structobj = PyObject_CallFunctionObjArgs(Struct, format, NULL); + Py_DECREF(format); + if (structobj == NULL) + goto out; + + unpack_from = PyObject_GetAttrString(structobj, "unpack_from"); + if (unpack_from == NULL) + goto out; + + item = PyMem_Malloc(base->itemsize); + if (item == NULL) { + PyErr_NoMemory(); + goto out; + } + + mview = PyMemoryView_FromMemory(item, base->itemsize, PyBUF_WRITE); + if (mview == NULL) + goto out; + + lst = unpack_rec(unpack_from, base->buf, mview, item, + shape, strides, base->suboffsets, + base->ndim, base->itemsize); + +out: + Py_XDECREF(mview); + PyMem_XFree(item); + Py_XDECREF(unpack_from); + Py_XDECREF(structobj); + if (strides != base->strides && strides != simple_strides) + PyMem_XFree(strides); + + return lst; +} + + +/****************************************************************************/ +/* Initialize ndbuf */ +/****************************************************************************/ + +/* + State of a new ndbuf during initialization. 'OK' means that initialization + is complete. 'PTR' means that a pointer has been initialized, but the + state of the memory is still undefined and ndbuf->offset is disregarded. + + +-----------------+-----------+-------------+----------------+ + | | ndbuf_new | init_simple | init_structure | + +-----------------+-----------+-------------+----------------+ + | next | OK (NULL) | OK | OK | + +-----------------+-----------+-------------+----------------+ + | prev | OK (NULL) | OK | OK | + +-----------------+-----------+-------------+----------------+ + | len | OK | OK | OK | + +-----------------+-----------+-------------+----------------+ + | offset | OK | OK | OK | + +-----------------+-----------+-------------+----------------+ + | data | PTR | OK | OK | + +-----------------+-----------+-------------+----------------+ + | flags | user | user | OK | + +-----------------+-----------+-------------+----------------+ + | exports | OK (0) | OK | OK | + +-----------------+-----------+-------------+----------------+ + | base.obj | OK (NULL) | OK | OK | + +-----------------+-----------+-------------+----------------+ + | base.buf | PTR | PTR | OK | + +-----------------+-----------+-------------+----------------+ + | base.len | len(data) | len(data) | OK | + +-----------------+-----------+-------------+----------------+ + | base.itemsize | 1 | OK | OK | + +-----------------+-----------+-------------+----------------+ + | base.readonly | 0 | OK | OK | + +-----------------+-----------+-------------+----------------+ + | base.format | NULL | OK | OK | + +-----------------+-----------+-------------+----------------+ + | base.ndim | 1 | 1 | OK | + +-----------------+-----------+-------------+----------------+ + | base.shape | NULL | NULL | OK | + +-----------------+-----------+-------------+----------------+ + | base.strides | NULL | NULL | OK | + +-----------------+-----------+-------------+----------------+ + | base.suboffsets | NULL | NULL | OK | + +-----------------+-----------+-------------+----------------+ + | base.internal | OK | OK | OK | + +-----------------+-----------+-------------+----------------+ + +*/ + +static Py_ssize_t +get_itemsize(PyObject *format) +{ + PyObject *tmp; + Py_ssize_t itemsize; + + tmp = PyObject_CallFunctionObjArgs(calcsize, format, NULL); + if (tmp == NULL) + return -1; + itemsize = PyLong_AsSsize_t(tmp); + Py_DECREF(tmp); + + return itemsize; +} + +static char * +get_format(PyObject *format) +{ + PyObject *tmp; + char *fmt; + + tmp = PyUnicode_AsASCIIString(format); + if (tmp == NULL) + return NULL; + fmt = PyMem_Malloc(PyBytes_GET_SIZE(tmp)+1); + if (fmt == NULL) { + PyErr_NoMemory(); + Py_DECREF(tmp); + return NULL; + } + strcpy(fmt, PyBytes_AS_STRING(tmp)); + Py_DECREF(tmp); + + return fmt; +} + +static int +init_simple(ndbuf_t *ndbuf, PyObject *items, PyObject *format, + Py_ssize_t itemsize) +{ + PyObject *mview; + Py_buffer *base = &ndbuf->base; + int ret; + + mview = PyMemoryView_FromBuffer(base); + if (mview == NULL) + return -1; + + ret = pack_from_list(mview, items, format, itemsize); + Py_DECREF(mview); + if (ret < 0) + return -1; + + base->readonly = !(ndbuf->flags & ND_WRITABLE); + base->itemsize = itemsize; + base->format = get_format(format); + if (base->format == NULL) + return -1; + + return 0; +} + +static Py_ssize_t * +seq_as_ssize_array(PyObject *seq, Py_ssize_t len, int is_shape) +{ + Py_ssize_t *dest; + Py_ssize_t x, i; + + dest = PyMem_Malloc(len * (sizeof *dest)); + if (dest == NULL) { + PyErr_NoMemory(); + return NULL; + } + + for (i = 0; i < len; i++) { + PyObject *tmp = PySequence_Fast_GET_ITEM(seq, i); + if (!PyLong_Check(tmp)) { + PyErr_Format(PyExc_ValueError, + "elements of %s must be integers", + is_shape ? "shape" : "strides"); + PyMem_Free(dest); + return NULL; + } + x = PyLong_AsSsize_t(tmp); + if (PyErr_Occurred()) { + PyMem_Free(dest); + return NULL; + } + if (is_shape && x < 0) { + PyErr_Format(PyExc_ValueError, + "elements of shape must be integers >= 0"); + PyMem_Free(dest); + return NULL; + } + dest[i] = x; + } + + return dest; +} + +static Py_ssize_t * +strides_from_shape(const ndbuf_t *ndbuf, int flags) +{ + const Py_buffer *base = &ndbuf->base; + Py_ssize_t *s, i; + + s = PyMem_Malloc(base->ndim * (sizeof *s)); + if (s == NULL) { + PyErr_NoMemory(); + return NULL; + } + + if (flags & ND_FORTRAN) { + s[0] = base->itemsize; + for (i = 1; i < base->ndim; i++) + s[i] = s[i-1] * base->shape[i-1]; + } + else { + s[base->ndim-1] = base->itemsize; + for (i = base->ndim-2; i >= 0; i--) + s[i] = s[i+1] * base->shape[i+1]; + } + + return s; +} + +/* Bounds check: + + len := complete length of allocated memory + offset := start of the array + + A single array element is indexed by: + + i = indices[0] * strides[0] + indices[1] * strides[1] + ... + + imin is reached when all indices[n] combined with positive strides are 0 + and all indices combined with negative strides are shape[n]-1, which is + the maximum index for the nth dimension. + + imax is reached when all indices[n] combined with negative strides are 0 + and all indices combined with positive strides are shape[n]-1. +*/ +static int +verify_structure(Py_ssize_t len, Py_ssize_t itemsize, Py_ssize_t offset, + const Py_ssize_t *shape, const Py_ssize_t *strides, + Py_ssize_t ndim) +{ + Py_ssize_t imin, imax; + Py_ssize_t n; + + assert(ndim >= 0); + + if (ndim == 0 && (offset < 0 || offset+itemsize > len)) + goto invalid_combination; + + for (n = 0; n < ndim; n++) + if (strides[n] % itemsize) { + PyErr_SetString(PyExc_ValueError, + "strides must be a multiple of itemsize"); + return -1; + } + + for (n = 0; n < ndim; n++) + if (shape[n] == 0) + return 0; + + imin = imax = 0; + for (n = 0; n < ndim; n++) + if (strides[n] <= 0) + imin += (shape[n]-1) * strides[n]; + else + imax += (shape[n]-1) * strides[n]; + + if (imin + offset < 0 || imax + offset + itemsize > len) + goto invalid_combination; + + return 0; + + +invalid_combination: + PyErr_SetString(PyExc_ValueError, + "invalid combination of buffer, shape and strides"); + return -1; +} + +/* + Convert a NumPy-style array to an array using suboffsets to stride in + the first dimension. Requirements: ndim > 0. + + Contiguous example + ================== + + Input: + ------ + shape = {2, 2, 3}; + strides = {6, 3, 1}; + suboffsets = NULL; + data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + buf = &data[0] + + Output: + ------- + shape = {2, 2, 3}; + strides = {sizeof(char *), 3, 1}; + suboffsets = {0, -1, -1}; + data = {p1, p2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + | | ^ ^ + `---'---' | + | | + `---------------------' + buf = &data[0] + + So, in the example the input resembles the three-dimensional array + char v[2][2][3], while the output resembles an array of two pointers + to two-dimensional arrays: char (*v[2])[2][3]. + + + Non-contiguous example: + ======================= + + Input (with offset and negative strides): + ----------------------------------------- + shape = {2, 2, 3}; + strides = {-6, 3, -1}; + offset = 8 + suboffsets = NULL; + data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + + Output: + ------- + shape = {2, 2, 3}; + strides = {-sizeof(char *), 3, -1}; + suboffsets = {2, -1, -1}; + newdata = {p1, p2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + | | ^ ^ ^ ^ + `---'---' | | `- p2+suboffsets[0] + | `-----------|--- p1+suboffsets[0] + `---------------------' + buf = &newdata[1] # striding backwards over the pointers. + + suboffsets[0] is the same as the offset that one would specify if + the two {2, 3} subarrays were created directly, hence the name. +*/ +static int +init_suboffsets(ndbuf_t *ndbuf) +{ + Py_buffer *base = &ndbuf->base; + Py_ssize_t start, step; + Py_ssize_t imin, suboffset0; + Py_ssize_t addsize; + Py_ssize_t n; + char *data; + + assert(base->ndim > 0); + assert(base->suboffsets == NULL); + + /* Allocate new data with additional space for shape[0] pointers. */ + addsize = base->shape[0] * (sizeof (char *)); + + /* Align array start to a multiple of 8. */ + addsize = 8 * ((addsize + 7) / 8); + + data = PyMem_Malloc(ndbuf->len + addsize); + if (data == NULL) { + PyErr_NoMemory(); + return -1; + } + + memcpy(data + addsize, ndbuf->data, ndbuf->len); + + PyMem_Free(ndbuf->data); + ndbuf->data = data; + ndbuf->len += addsize; + base->buf = ndbuf->data; + + /* imin: minimum index of the input array relative to ndbuf->offset. + suboffset0: offset for each sub-array of the output. This is the + same as calculating -imin' for a sub-array of ndim-1. */ + imin = suboffset0 = 0; + for (n = 0; n < base->ndim; n++) { + if (base->shape[n] == 0) + break; + if (base->strides[n] <= 0) { + Py_ssize_t x = (base->shape[n]-1) * base->strides[n]; + imin += x; + suboffset0 += (n >= 1) ? -x : 0; + } + } + + /* Initialize the array of pointers to the sub-arrays. */ + start = addsize + ndbuf->offset + imin; + step = base->strides[0] < 0 ? -base->strides[0] : base->strides[0]; + + for (n = 0; n < base->shape[0]; n++) + ((char **)base->buf)[n] = (char *)base->buf + start + n*step; + + /* Initialize suboffsets. */ + base->suboffsets = PyMem_Malloc(base->ndim * (sizeof *base->suboffsets)); + if (base->suboffsets == NULL) { + PyErr_NoMemory(); + return -1; + } + base->suboffsets[0] = suboffset0; + for (n = 1; n < base->ndim; n++) + base->suboffsets[n] = -1; + + /* Adjust strides for the first (zeroth) dimension. */ + if (base->strides[0] >= 0) { + base->strides[0] = sizeof(char *); + } + else { + /* Striding backwards. */ + base->strides[0] = -(Py_ssize_t)sizeof(char *); + if (base->shape[0] > 0) + base->buf = (char *)base->buf + (base->shape[0]-1) * sizeof(char *); + } + + ndbuf->flags &= ~(ND_C|ND_FORTRAN); + ndbuf->offset = 0; + return 0; +} + +static void +init_len(Py_buffer *base) +{ + Py_ssize_t i; + + base->len = 1; + for (i = 0; i < base->ndim; i++) + base->len *= base->shape[i]; + base->len *= base->itemsize; +} + +static int +init_structure(ndbuf_t *ndbuf, PyObject *shape, PyObject *strides, + Py_ssize_t ndim) +{ + Py_buffer *base = &ndbuf->base; + + base->ndim = (int)ndim; + if (ndim == 0) { + if (ndbuf->flags & ND_PIL) { + PyErr_SetString(PyExc_TypeError, + "ndim = 0 cannot be used in conjunction with ND_PIL"); + return -1; + } + ndbuf->flags |= (ND_SCALAR|ND_C|ND_FORTRAN); + return 0; + } + + /* shape */ + base->shape = seq_as_ssize_array(shape, ndim, 1); + if (base->shape == NULL) + return -1; + + /* strides */ + if (strides) { + base->strides = seq_as_ssize_array(strides, ndim, 0); + } + else { + base->strides = strides_from_shape(ndbuf, ndbuf->flags); + } + if (base->strides == NULL) + return -1; + if (verify_structure(base->len, base->itemsize, ndbuf->offset, + base->shape, base->strides, ndim) < 0) + return -1; + + /* buf */ + base->buf = ndbuf->data + ndbuf->offset; + + /* len */ + init_len(base); + + /* ndbuf->flags */ + if (PyBuffer_IsContiguous(base, 'C')) + ndbuf->flags |= ND_C; + if (PyBuffer_IsContiguous(base, 'F')) + ndbuf->flags |= ND_FORTRAN; + + + /* convert numpy array to suboffset representation */ + if (ndbuf->flags & ND_PIL) { + /* modifies base->buf, base->strides and base->suboffsets **/ + return init_suboffsets(ndbuf); + } + + return 0; +} + +static ndbuf_t * +init_ndbuf(PyObject *items, PyObject *shape, PyObject *strides, + Py_ssize_t offset, PyObject *format, int flags) +{ + ndbuf_t *ndbuf; + Py_ssize_t ndim; + Py_ssize_t nitems; + Py_ssize_t itemsize; + + /* ndim = len(shape) */ + CHECK_LIST_OR_TUPLE(shape) + ndim = PySequence_Fast_GET_SIZE(shape); + if (ndim > ND_MAX_NDIM) { + PyErr_Format(PyExc_ValueError, + "ndim must not exceed %d", ND_MAX_NDIM); + return NULL; + } + + /* len(strides) = len(shape) */ + if (strides) { + CHECK_LIST_OR_TUPLE(strides) + if (PySequence_Fast_GET_SIZE(strides) == 0) + strides = NULL; + else if (flags & ND_FORTRAN) { + PyErr_SetString(PyExc_TypeError, + "ND_FORTRAN cannot be used together with strides"); + return NULL; + } + else if (PySequence_Fast_GET_SIZE(strides) != ndim) { + PyErr_SetString(PyExc_ValueError, + "len(shape) != len(strides)"); + return NULL; + } + } + + /* itemsize */ + itemsize = get_itemsize(format); + if (itemsize <= 0) { + if (itemsize == 0) { + PyErr_SetString(PyExc_ValueError, + "itemsize must not be zero"); + } + return NULL; + } + + /* convert scalar to list */ + if (ndim == 0) { + items = Py_BuildValue("(O)", items); + if (items == NULL) + return NULL; + } + else { + CHECK_LIST_OR_TUPLE(items) + Py_INCREF(items); + } + + /* number of items */ + nitems = PySequence_Fast_GET_SIZE(items); + if (nitems == 0) { + PyErr_SetString(PyExc_ValueError, + "initializer list or tuple must not be empty"); + Py_DECREF(items); + return NULL; + } + + ndbuf = ndbuf_new(nitems, itemsize, offset, flags); + if (ndbuf == NULL) { + Py_DECREF(items); + return NULL; + } + + + if (init_simple(ndbuf, items, format, itemsize) < 0) + goto error; + if (init_structure(ndbuf, shape, strides, ndim) < 0) + goto error; + + Py_DECREF(items); + return ndbuf; + +error: + Py_DECREF(items); + ndbuf_free(ndbuf); + return NULL; +} + +/* initialize and push a new base onto the linked list */ +static int +ndarray_push_base(NDArrayObject *nd, PyObject *items, + PyObject *shape, PyObject *strides, + Py_ssize_t offset, PyObject *format, int flags) +{ + ndbuf_t *ndbuf; + + ndbuf = init_ndbuf(items, shape, strides, offset, format, flags); + if (ndbuf == NULL) + return -1; + + ndbuf_push(nd, ndbuf); + return 0; +} + +#define PyBUF_UNUSED 0x10000 +static int +ndarray_init(PyObject *self, PyObject *args, PyObject *kwds) +{ + NDArrayObject *nd = (NDArrayObject *)self; + static char *kwlist[] = { + "obj", "shape", "strides", "offset", "format", "flags", "getbuf", NULL + }; + PyObject *v = NULL; /* initializer: scalar, list, tuple or base object */ + PyObject *shape = NULL; /* size of each dimension */ + PyObject *strides = NULL; /* number of bytes to the next elt in each dim */ + Py_ssize_t offset = 0; /* buffer offset */ + PyObject *format = simple_format; /* struct module specifier: "B" */ + int flags = ND_UNUSED; /* base buffer and ndarray flags */ + + int getbuf = PyBUF_UNUSED; /* re-exporter: getbuffer request flags */ + + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOnOii", kwlist, + &v, &shape, &strides, &offset, &format, &flags, &getbuf)) + return -1; + + /* NDArrayObject is re-exporter */ + if (PyObject_CheckBuffer(v) && shape == NULL) { + if (strides || offset || format != simple_format || + flags != ND_UNUSED) { + PyErr_SetString(PyExc_TypeError, + "construction from exporter object only takes a single " + "additional getbuf argument"); + return -1; + } + + getbuf = (getbuf == PyBUF_UNUSED) ? PyBUF_FULL_RO : getbuf; + + if (ndarray_init_staticbuf(v, nd, getbuf) < 0) + return -1; + + init_flags(nd->head); + + return 0; + } + + /* NDArrayObject is the original base object. */ + if (getbuf != PyBUF_UNUSED) { + PyErr_SetString(PyExc_TypeError, + "getbuf argument only valid for construction from exporter " + "object"); + return -1; + } + if (shape == NULL) { + PyErr_SetString(PyExc_TypeError, + "shape is a required argument when constructing from " + "list, tuple or scalar"); + return -1; + } + + if (flags == ND_UNUSED) + flags = ND_DEFAULT; + if (flags & ND_VAREXPORT) { + nd->flags |= ND_VAREXPORT; + flags &= ~ND_VAREXPORT; + } + + /* Initialize and push the first base buffer onto the linked list. */ + return ndarray_push_base(nd, v, shape, strides, offset, format, flags); +} + +/* Push an additional base onto the linked list. */ +static PyObject * +ndarray_push(PyObject *self, PyObject *args, PyObject *kwds) +{ + NDArrayObject *nd = (NDArrayObject *)self; + static char *kwlist[] = { + "items", "shape", "strides", "offset", "format", "flags", NULL + }; + PyObject *items = NULL; /* initializer: scalar, list or tuple */ + PyObject *shape = NULL; /* size of each dimension */ + PyObject *strides = NULL; /* number of bytes to the next elt in each dim */ + PyObject *format = simple_format; /* struct module specifier: "B" */ + Py_ssize_t offset = 0; /* buffer offset */ + int flags = ND_UNUSED; /* base buffer flags */ + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OnOi", kwlist, + &items, &shape, &strides, &offset, &format, &flags)) + return NULL; + + if (flags & ND_VAREXPORT) { + PyErr_SetString(PyExc_ValueError, + "ND_VAREXPORT flag can only be used during object creation"); + return NULL; + } + if (ND_IS_CONSUMER(nd)) { + PyErr_SetString(PyExc_BufferError, + "structure of re-exporting object is immutable"); + return NULL; + } + if (!(nd->flags&ND_VAREXPORT) && nd->head->exports > 0) { + PyErr_Format(PyExc_BufferError, + "cannot change structure: %zd exported buffer%s", + nd->head->exports, nd->head->exports==1 ? "" : "s"); + return NULL; + } + + if (ndarray_push_base(nd, items, shape, strides, + offset, format, flags) < 0) + return NULL; + Py_RETURN_NONE; +} + +/* Pop a base from the linked list (if possible). */ +static PyObject * +ndarray_pop(PyObject *self, PyObject *dummy) +{ + NDArrayObject *nd = (NDArrayObject *)self; + if (ND_IS_CONSUMER(nd)) { + PyErr_SetString(PyExc_BufferError, + "structure of re-exporting object is immutable"); + return NULL; + } + if (nd->head->exports > 0) { + PyErr_Format(PyExc_BufferError, + "cannot change structure: %zd exported buffer%s", + nd->head->exports, nd->head->exports==1 ? "" : "s"); + return NULL; + } + if (nd->head->next == NULL) { + PyErr_SetString(PyExc_BufferError, + "list only has a single base"); + return NULL; + } + + ndbuf_pop(nd); + Py_RETURN_NONE; +} + +/**************************************************************************/ +/* getbuffer */ +/**************************************************************************/ + +static int +ndarray_getbuf(NDArrayObject *self, Py_buffer *view, int flags) +{ + ndbuf_t *ndbuf = self->head; + Py_buffer *base = &ndbuf->base; + int baseflags = ndbuf->flags; + + /* start with complete information */ + *view = *base; + view->obj = NULL; + + /* reconstruct format */ + if (view->format == NULL) + view->format = "B"; + + if (base->ndim != 0 && + ((REQ_SHAPE(flags) && base->shape == NULL) || + (REQ_STRIDES(flags) && base->strides == NULL))) { + /* The ndarray is a re-exporter that has been created without full + information for testing purposes. In this particular case the + ndarray is not a PEP-3118 compliant buffer provider. */ + PyErr_SetString(PyExc_BufferError, + "re-exporter does not provide format, shape or strides"); + return -1; + } + + if (baseflags & ND_GETBUF_FAIL) { + PyErr_SetString(PyExc_BufferError, + "ND_GETBUF_FAIL: forced test exception"); + return -1; + } + + if (REQ_WRITABLE(flags) && base->readonly) { + PyErr_SetString(PyExc_BufferError, + "ndarray is not writable"); + return -1; + } + if (!REQ_FORMAT(flags)) { + /* NULL indicates that the buffer's data type has been cast to 'B'. + view->itemsize is the _previous_ itemsize. If shape is present, + the equality product(shape) * itemsize = len still holds at this + point. The equality calcsize(format) = itemsize does _not_ hold + from here on! */ + view->format = NULL; + } + + if (REQ_C_CONTIGUOUS(flags) && !ND_C_CONTIGUOUS(baseflags)) { + PyErr_SetString(PyExc_BufferError, + "ndarray is not C-contiguous"); + return -1; + } + if (REQ_F_CONTIGUOUS(flags) && !ND_FORTRAN_CONTIGUOUS(baseflags)) { + PyErr_SetString(PyExc_BufferError, + "ndarray is not Fortran contiguous"); + return -1; + } + if (REQ_ANY_CONTIGUOUS(flags) && !ND_ANY_CONTIGUOUS(baseflags)) { + PyErr_SetString(PyExc_BufferError, + "ndarray is not contiguous"); + return -1; + } + if (!REQ_INDIRECT(flags) && (baseflags & ND_PIL)) { + PyErr_SetString(PyExc_BufferError, + "ndarray cannot be represented without suboffsets"); + return -1; + } + if (!REQ_STRIDES(flags)) { + if (!ND_C_CONTIGUOUS(baseflags)) { + PyErr_SetString(PyExc_BufferError, + "ndarray is not C-contiguous"); + return -1; + } + view->strides = NULL; + } + if (!REQ_SHAPE(flags)) { + /* PyBUF_SIMPLE or PyBUF_WRITABLE: at this point buf is C-contiguous, + so base->buf = ndbuf->data. */ + if (view->format != NULL) { + /* PyBUF_SIMPLE|PyBUF_FORMAT and PyBUF_WRITABLE|PyBUF_FORMAT do + not make sense. */ + PyErr_Format(PyExc_BufferError, + "ndarray: cannot cast to unsigned bytes if the format flag " + "is present"); + return -1; + } + /* product(shape) * itemsize = len and calcsize(format) = itemsize + do _not_ hold from here on! */ + view->ndim = 1; + view->shape = NULL; + } + + view->obj = (PyObject *)self; + Py_INCREF(view->obj); + self->head->exports++; + + return 0; +} + +static int +ndarray_releasebuf(NDArrayObject *self, Py_buffer *view) +{ + if (!ND_IS_CONSUMER(self)) { + ndbuf_t *ndbuf = view->internal; + if (--ndbuf->exports == 0 && ndbuf != self->head) + ndbuf_delete(self, ndbuf); + } + + return 0; +} + +static PyBufferProcs ndarray_as_buffer = { + (getbufferproc)ndarray_getbuf, /* bf_getbuffer */ + (releasebufferproc)ndarray_releasebuf /* bf_releasebuffer */ +}; + + +/**************************************************************************/ +/* indexing/slicing */ +/**************************************************************************/ + +static char * +ptr_from_index(Py_buffer *base, Py_ssize_t index) +{ + char *ptr; + Py_ssize_t nitems; /* items in the first dimension */ + + if (base->shape) + nitems = base->shape[0]; + else { + assert(base->ndim == 1 && SIMPLE_FORMAT(base->format)); + nitems = base->len; + } + + if (index < 0) { + index += nitems; + } + if (index < 0 || index >= nitems) { + PyErr_SetString(PyExc_IndexError, "index out of bounds"); + return NULL; + } + + ptr = (char *)base->buf; + + if (base->strides == NULL) + ptr += base->itemsize * index; + else + ptr += base->strides[0] * index; + + ptr = ADJUST_PTR(ptr, base->suboffsets); + + return ptr; +} + +static PyObject * +ndarray_item(NDArrayObject *self, Py_ssize_t index) +{ + ndbuf_t *ndbuf = self->head; + Py_buffer *base = &ndbuf->base; + char *ptr; + + if (base->ndim == 0) { + PyErr_SetString(PyExc_TypeError, "invalid indexing of scalar"); + return NULL; + } + + ptr = ptr_from_index(base, index); + if (ptr == NULL) + return NULL; + + if (base->ndim == 1) { + return unpack_single(ptr, base->format, base->itemsize); + } + else { + NDArrayObject *nd; + Py_buffer *subview; + + nd = (NDArrayObject *)ndarray_new(&NDArray_Type, NULL, NULL); + if (nd == NULL) + return NULL; + + if (ndarray_init_staticbuf((PyObject *)self, nd, PyBUF_FULL_RO) < 0) { + Py_DECREF(nd); + return NULL; + } + + subview = &nd->staticbuf.base; + + subview->buf = ptr; + subview->len /= subview->shape[0]; + + subview->ndim--; + subview->shape++; + if (subview->strides) subview->strides++; + if (subview->suboffsets) subview->suboffsets++; + + init_flags(&nd->staticbuf); + + return (PyObject *)nd; + } +} + +/* + For each dimension, we get valid (start, stop, step, slicelength) quadruples + from PySlice_GetIndicesEx(). + + Slicing NumPy arrays + ==================== + + A pointer to an element in a NumPy array is defined by: + + ptr = (char *)buf + indices[0] * strides[0] + + ... + + indices[ndim-1] * strides[ndim-1] + + Adjust buf: + ----------- + Adding start[n] for each dimension effectively adds the constant: + + c = start[0] * strides[0] + ... + start[ndim-1] * strides[ndim-1] + + Therefore init_slice() adds all start[n] directly to buf. + + Adjust shape: + ------------- + Obviously shape[n] = slicelength[n] + + Adjust strides: + --------------- + In the original array, the next element in a dimension is reached + by adding strides[n] to the pointer. In the sliced array, elements + may be skipped, so the next element is reached by adding: + + strides[n] * step[n] + + Slicing PIL arrays + ================== + + Layout: + ------- + In the first (zeroth) dimension, PIL arrays have an array of pointers + to sub-arrays of ndim-1. Striding in the first dimension is done by + getting the index of the nth pointer, dereference it and then add a + suboffset to it. The arrays pointed to can best be seen a regular + NumPy arrays. + + Adjust buf: + ----------- + In the original array, buf points to a location (usually the start) + in the array of pointers. For the sliced array, start[0] can be + added to buf in the same manner as for NumPy arrays. + + Adjust suboffsets: + ------------------ + Due to the dereferencing step in the addressing scheme, it is not + possible to adjust buf for higher dimensions. Recall that the + sub-arrays pointed to are regular NumPy arrays, so for each of + those arrays adding start[n] effectively adds the constant: + + c = start[1] * strides[1] + ... + start[ndim-1] * strides[ndim-1] + + This constant is added to suboffsets[0]. suboffsets[0] in turn is + added to each pointer right after dereferencing. + + Adjust shape and strides: + ------------------------- + Shape and strides are not influenced by the dereferencing step, so + they are adjusted in the same manner as for NumPy arrays. + + Multiple levels of suboffsets + ============================= + + For a construct like an array of pointers to array of pointers to + sub-arrays of ndim-2: + + suboffsets[0] = start[1] * strides[1] + suboffsets[1] = start[2] * strides[2] + ... +*/ +static int +init_slice(Py_buffer *base, PyObject *key, int dim) +{ + Py_ssize_t start, stop, step, slicelength; + + if (PySlice_GetIndicesEx(key, base->shape[dim], + &start, &stop, &step, &slicelength) < 0) { + return -1; + } + + + if (base->suboffsets == NULL || dim == 0) { + adjust_buf: + base->buf = (char *)base->buf + base->strides[dim] * start; + } + else { + Py_ssize_t n = dim-1; + while (n >= 0 && base->suboffsets[n] < 0) + n--; + if (n < 0) + goto adjust_buf; /* all suboffsets are negative */ + base->suboffsets[n] = base->suboffsets[n] + base->strides[dim] * start; + } + base->shape[dim] = slicelength; + base->strides[dim] = base->strides[dim] * step; + + return 0; +} + +static int +copy_structure(Py_buffer *base) +{ + Py_ssize_t *shape = NULL, *strides = NULL, *suboffsets = NULL; + Py_ssize_t i; + + shape = PyMem_Malloc(base->ndim * (sizeof *shape)); + strides = PyMem_Malloc(base->ndim * (sizeof *strides)); + if (shape == NULL || strides == NULL) + goto err_nomem; + + suboffsets = NULL; + if (base->suboffsets) { + suboffsets = PyMem_Malloc(base->ndim * (sizeof *suboffsets)); + if (suboffsets == NULL) + goto err_nomem; + } + + for (i = 0; i < base->ndim; i++) { + shape[i] = base->shape[i]; + strides[i] = base->strides[i]; + if (suboffsets) + suboffsets[i] = base->suboffsets[i]; + } + + base->shape = shape; + base->strides = strides; + base->suboffsets = suboffsets; + + return 0; + +err_nomem: + PyErr_NoMemory(); + PyMem_XFree(shape); + PyMem_XFree(strides); + PyMem_XFree(suboffsets); + return -1; +} + +static PyObject * +ndarray_subscript(NDArrayObject *self, PyObject *key) +{ + NDArrayObject *nd; + ndbuf_t *ndbuf; + Py_buffer *base = &self->head->base; + + if (base->ndim == 0) { + if (PyTuple_Check(key) && PyTuple_GET_SIZE(key) == 0) { + return unpack_single(base->buf, base->format, base->itemsize); + } + else if (key == Py_Ellipsis) { + Py_INCREF(self); + return (PyObject *)self; + } + else { + PyErr_SetString(PyExc_TypeError, "invalid indexing of scalar"); + return NULL; + } + } + if (PyIndex_Check(key)) { + Py_ssize_t index = PyLong_AsSsize_t(key); + if (index == -1 && PyErr_Occurred()) + return NULL; + return ndarray_item(self, index); + } + + nd = (NDArrayObject *)ndarray_new(&NDArray_Type, NULL, NULL); + if (nd == NULL) + return NULL; + + /* new ndarray is a consumer */ + if (ndarray_init_staticbuf((PyObject *)self, nd, PyBUF_FULL_RO) < 0) { + Py_DECREF(nd); + return NULL; + } + + /* copy shape, strides and suboffsets */ + ndbuf = nd->head; + base = &ndbuf->base; + if (copy_structure(base) < 0) { + Py_DECREF(nd); + return NULL; + } + ndbuf->flags |= ND_OWN_ARRAYS; + + if (PySlice_Check(key)) { + /* one-dimensional slice */ + if (init_slice(base, key, 0) < 0) + goto err_occurred; + } + else if PyTuple_Check(key) { + /* multi-dimensional slice */ + PyObject *tuple = key; + Py_ssize_t i, n; + + n = PyTuple_GET_SIZE(tuple); + for (i = 0; i < n; i++) { + key = PyTuple_GET_ITEM(tuple, i); + if (!PySlice_Check(key)) + goto type_error; + if (init_slice(base, key, (int)i) < 0) + goto err_occurred; + } + } + else { + goto type_error; + } + + init_len(base); + init_flags(ndbuf); + + return (PyObject *)nd; + + +type_error: + PyErr_Format(PyExc_TypeError, + "cannot index memory using \"%.200s\"", + key->ob_type->tp_name); +err_occurred: + Py_DECREF(nd); + return NULL; +} + + +static int +ndarray_ass_subscript(NDArrayObject *self, PyObject *key, PyObject *value) +{ + NDArrayObject *nd; + Py_buffer *dest = &self->head->base; + Py_buffer src; + char *ptr; + Py_ssize_t index; + int ret = -1; + + if (dest->readonly) { + PyErr_SetString(PyExc_TypeError, "ndarray is not writable"); + return -1; + } + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "ndarray data cannot be deleted"); + return -1; + } + if (dest->ndim == 0) { + if (key == Py_Ellipsis || + (PyTuple_Check(key) && PyTuple_GET_SIZE(key) == 0)) { + ptr = (char *)dest->buf; + return pack_single(ptr, value, dest->format, dest->itemsize); + } + else { + PyErr_SetString(PyExc_TypeError, "invalid indexing of scalar"); + return -1; + } + } + if (dest->ndim == 1 && PyIndex_Check(key)) { + /* rvalue must be a single item */ + index = PyLong_AsSsize_t(key); + if (index == -1 && PyErr_Occurred()) + return -1; + else { + ptr = ptr_from_index(dest, index); + if (ptr == NULL) + return -1; + } + return pack_single(ptr, value, dest->format, dest->itemsize); + } + + /* rvalue must be an exporter */ + if (PyObject_GetBuffer(value, &src, PyBUF_FULL_RO) == -1) + return -1; + + nd = (NDArrayObject *)ndarray_subscript(self, key); + if (nd != NULL) { + dest = &nd->head->base; + ret = copy_buffer(dest, &src); + Py_DECREF(nd); + } + + PyBuffer_Release(&src); + return ret; +} + +static PyObject * +slice_indices(PyObject *self, PyObject *args) +{ + PyObject *ret, *key, *tmp; + Py_ssize_t s[4]; /* start, stop, step, slicelength */ + Py_ssize_t i, len; + + if (!PyArg_ParseTuple(args, "On", &key, &len)) { + return NULL; + } + if (!PySlice_Check(key)) { + PyErr_SetString(PyExc_TypeError, + "first argument must be a slice object"); + return NULL; + } + if (PySlice_GetIndicesEx(key, len, &s[0], &s[1], &s[2], &s[3]) < 0) { + return NULL; + } + + ret = PyTuple_New(4); + if (ret == NULL) + return NULL; + + for (i = 0; i < 4; i++) { + tmp = PyLong_FromSsize_t(s[i]); + if (tmp == NULL) + goto error; + PyTuple_SET_ITEM(ret, i, tmp); + } + + return ret; + +error: + Py_DECREF(ret); + return NULL; +} + + +static PyMappingMethods ndarray_as_mapping = { + NULL, /* mp_length */ + (binaryfunc)ndarray_subscript, /* mp_subscript */ + (objobjargproc)ndarray_ass_subscript /* mp_ass_subscript */ +}; + +static PySequenceMethods ndarray_as_sequence = { + 0, /* sq_length */ + 0, /* sq_concat */ + 0, /* sq_repeat */ + (ssizeargfunc)ndarray_item, /* sq_item */ +}; + + +/**************************************************************************/ +/* getters */ +/**************************************************************************/ + +static PyObject * +ssize_array_as_tuple(Py_ssize_t *array, Py_ssize_t len) +{ + PyObject *tuple, *x; + Py_ssize_t i; + + if (array == NULL) + return PyTuple_New(0); + + tuple = PyTuple_New(len); + if (tuple == NULL) + return NULL; + + for (i = 0; i < len; i++) { + x = PyLong_FromSsize_t(array[i]); + if (x == NULL) { + Py_DECREF(tuple); + return NULL; + } + PyTuple_SET_ITEM(tuple, i, x); + } + + return tuple; +} + +static PyObject * +ndarray_get_flags(NDArrayObject *self, void *closure) +{ + return PyLong_FromLong(self->head->flags); +} + +static PyObject * +ndarray_get_offset(NDArrayObject *self, void *closure) +{ + ndbuf_t *ndbuf = self->head; + return PyLong_FromSsize_t(ndbuf->offset); +} + +static PyObject * +ndarray_get_obj(NDArrayObject *self, void *closure) +{ + Py_buffer *base = &self->head->base; + + if (base->obj == NULL) { + Py_RETURN_NONE; + } + Py_INCREF(base->obj); + return base->obj; +} + +static PyObject * +ndarray_get_nbytes(NDArrayObject *self, void *closure) +{ + Py_buffer *base = &self->head->base; + return PyLong_FromSsize_t(base->len); +} + +static PyObject * +ndarray_get_readonly(NDArrayObject *self, void *closure) +{ + Py_buffer *base = &self->head->base; + return PyLong_FromLong(base->readonly); +} + +static PyObject * +ndarray_get_itemsize(NDArrayObject *self, void *closure) +{ + Py_buffer *base = &self->head->base; + return PyLong_FromSsize_t(base->itemsize); +} + +static PyObject * +ndarray_get_format(NDArrayObject *self, void *closure) +{ + Py_buffer *base = &self->head->base; + char *fmt = base->format ? base->format : ""; + return PyUnicode_FromString(fmt); +} + +static PyObject * +ndarray_get_ndim(NDArrayObject *self, void *closure) +{ + Py_buffer *base = &self->head->base; + return PyLong_FromSsize_t(base->ndim); +} + +static PyObject * +ndarray_get_shape(NDArrayObject *self, void *closure) +{ + Py_buffer *base = &self->head->base; + return ssize_array_as_tuple(base->shape, base->ndim); +} + +static PyObject * +ndarray_get_strides(NDArrayObject *self, void *closure) +{ + Py_buffer *base = &self->head->base; + return ssize_array_as_tuple(base->strides, base->ndim); +} + +static PyObject * +ndarray_get_suboffsets(NDArrayObject *self, void *closure) +{ + Py_buffer *base = &self->head->base; + return ssize_array_as_tuple(base->suboffsets, base->ndim); +} + +static PyObject * +ndarray_c_contig(PyObject *self, PyObject *dummy) +{ + NDArrayObject *nd = (NDArrayObject *)self; + int ret = PyBuffer_IsContiguous(&nd->head->base, 'C'); + + if (ret != ND_C_CONTIGUOUS(nd->head->flags)) { + PyErr_SetString(PyExc_RuntimeError, + "results from PyBuffer_IsContiguous() and flags differ"); + return NULL; + } + return PyBool_FromLong(ret); +} + +static PyObject * +ndarray_fortran_contig(PyObject *self, PyObject *dummy) +{ + NDArrayObject *nd = (NDArrayObject *)self; + int ret = PyBuffer_IsContiguous(&nd->head->base, 'F'); + + if (ret != ND_FORTRAN_CONTIGUOUS(nd->head->flags)) { + PyErr_SetString(PyExc_RuntimeError, + "results from PyBuffer_IsContiguous() and flags differ"); + return NULL; + } + return PyBool_FromLong(ret); +} + +static PyObject * +ndarray_contig(PyObject *self, PyObject *dummy) +{ + NDArrayObject *nd = (NDArrayObject *)self; + int ret = PyBuffer_IsContiguous(&nd->head->base, 'A'); + + if (ret != ND_ANY_CONTIGUOUS(nd->head->flags)) { + PyErr_SetString(PyExc_RuntimeError, + "results from PyBuffer_IsContiguous() and flags differ"); + return NULL; + } + return PyBool_FromLong(ret); +} + + +static PyGetSetDef ndarray_getset [] = +{ + /* ndbuf */ + { "flags", (getter)ndarray_get_flags, NULL, NULL, NULL}, + { "offset", (getter)ndarray_get_offset, NULL, NULL, NULL}, + /* ndbuf.base */ + { "obj", (getter)ndarray_get_obj, NULL, NULL, NULL}, + { "nbytes", (getter)ndarray_get_nbytes, NULL, NULL, NULL}, + { "readonly", (getter)ndarray_get_readonly, NULL, NULL, NULL}, + { "itemsize", (getter)ndarray_get_itemsize, NULL, NULL, NULL}, + { "format", (getter)ndarray_get_format, NULL, NULL, NULL}, + { "ndim", (getter)ndarray_get_ndim, NULL, NULL, NULL}, + { "shape", (getter)ndarray_get_shape, NULL, NULL, NULL}, + { "strides", (getter)ndarray_get_strides, NULL, NULL, NULL}, + { "suboffsets", (getter)ndarray_get_suboffsets, NULL, NULL, NULL}, + { "c_contiguous", (getter)ndarray_c_contig, NULL, NULL, NULL}, + { "f_contiguous", (getter)ndarray_fortran_contig, NULL, NULL, NULL}, + { "contiguous", (getter)ndarray_contig, NULL, NULL, NULL}, + {NULL} +}; + +static PyObject * +ndarray_tolist(PyObject *self, PyObject *dummy) +{ + return ndarray_as_list((NDArrayObject *)self); +} + +static PyObject * +ndarray_tobytes(PyObject *self, PyObject *dummy) +{ + ndbuf_t *ndbuf = ((NDArrayObject *)self)->head; + Py_buffer *src = &ndbuf->base; + Py_buffer dest; + PyObject *ret = NULL; + char *mem; + + if (ND_C_CONTIGUOUS(ndbuf->flags)) + return PyBytes_FromStringAndSize(src->buf, src->len); + + assert(src->shape != NULL); + assert(src->strides != NULL); + assert(src->ndim > 0); + + mem = PyMem_Malloc(src->len); + if (mem == NULL) { + PyErr_NoMemory(); + return NULL; + } + + dest = *src; + dest.buf = mem; + dest.suboffsets = NULL; + dest.strides = strides_from_shape(ndbuf, 0); + if (dest.strides == NULL) + goto out; + if (copy_buffer(&dest, src) < 0) + goto out; + + ret = PyBytes_FromStringAndSize(mem, src->len); + +out: + PyMem_XFree(dest.strides); + PyMem_Free(mem); + return ret; +} + +/* add redundant (negative) suboffsets for testing */ +static PyObject * +ndarray_add_suboffsets(PyObject *self, PyObject *dummy) +{ + NDArrayObject *nd = (NDArrayObject *)self; + Py_buffer *base = &nd->head->base; + Py_ssize_t i; + + if (base->suboffsets != NULL) { + PyErr_SetString(PyExc_TypeError, + "cannot add suboffsets to PIL-style array"); + return NULL; + } + if (base->strides == NULL) { + PyErr_SetString(PyExc_TypeError, + "cannot add suboffsets to array without strides"); + return NULL; + } + + base->suboffsets = PyMem_Malloc(base->ndim * (sizeof *base->suboffsets)); + if (base->suboffsets == NULL) { + PyErr_NoMemory(); + return NULL; + } + + for (i = 0; i < base->ndim; i++) + base->suboffsets[i] = -1; + + Py_RETURN_NONE; +} + +/* Test PyMemoryView_FromBuffer(): return a memoryview from a static buffer. + Obviously this is fragile and only one such view may be active at any + time. Never use anything like this in real code! */ +static char *infobuf = NULL; +static PyObject * +ndarray_memoryview_from_buffer(PyObject *self, PyObject *dummy) +{ + const NDArrayObject *nd = (NDArrayObject *)self; + const Py_buffer *view = &nd->head->base; + const ndbuf_t *ndbuf; + static char format[ND_MAX_NDIM+1]; + static Py_ssize_t shape[ND_MAX_NDIM]; + static Py_ssize_t strides[ND_MAX_NDIM]; + static Py_ssize_t suboffsets[ND_MAX_NDIM]; + static Py_buffer info; + char *p; + + if (!ND_IS_CONSUMER(nd)) + ndbuf = nd->head; /* self is ndarray/original exporter */ + else if (NDArray_Check(view->obj) && !ND_IS_CONSUMER(view->obj)) + /* self is ndarray and consumer from ndarray/original exporter */ + ndbuf = ((NDArrayObject *)view->obj)->head; + else { + PyErr_SetString(PyExc_TypeError, + "memoryview_from_buffer(): ndarray must be original exporter or " + "consumer from ndarray/original exporter"); + return NULL; + } + + info = *view; + p = PyMem_Realloc(infobuf, ndbuf->len); + if (p == NULL) { + PyMem_Free(infobuf); + PyErr_NoMemory(); + infobuf = NULL; + return NULL; + } + else { + infobuf = p; + } + /* copy the complete raw data */ + memcpy(infobuf, ndbuf->data, ndbuf->len); + info.buf = infobuf + ((char *)view->buf - ndbuf->data); + + if (view->format) { + if (strlen(view->format) > ND_MAX_NDIM) { + PyErr_Format(PyExc_TypeError, + "memoryview_from_buffer: format is limited to %d characters", + ND_MAX_NDIM); + return NULL; + } + strcpy(format, view->format); + info.format = format; + } + if (view->ndim > ND_MAX_NDIM) { + PyErr_Format(PyExc_TypeError, + "memoryview_from_buffer: ndim is limited to %d", ND_MAX_NDIM); + return NULL; + } + if (view->shape) { + memcpy(shape, view->shape, view->ndim * sizeof(Py_ssize_t)); + info.shape = shape; + } + if (view->strides) { + memcpy(strides, view->strides, view->ndim * sizeof(Py_ssize_t)); + info.strides = strides; + } + if (view->suboffsets) { + memcpy(suboffsets, view->suboffsets, view->ndim * sizeof(Py_ssize_t)); + info.suboffsets = suboffsets; + } + + return PyMemoryView_FromBuffer(&info); +} + +/* Get a single item from bufobj at the location specified by seq. + seq is a list or tuple of indices. The purpose of this function + is to check other functions against PyBuffer_GetPointer(). */ +static PyObject * +get_pointer(PyObject *self, PyObject *args) +{ + PyObject *ret = NULL, *bufobj, *seq; + Py_buffer view; + Py_ssize_t indices[ND_MAX_NDIM]; + Py_ssize_t i; + void *ptr; + + if (!PyArg_ParseTuple(args, "OO", &bufobj, &seq)) { + return NULL; + } + + CHECK_LIST_OR_TUPLE(seq); + if (PyObject_GetBuffer(bufobj, &view, PyBUF_FULL_RO) < 0) + return NULL; + + if (view.ndim > ND_MAX_NDIM) { + PyErr_Format(PyExc_ValueError, + "get_pointer(): ndim > %d", ND_MAX_NDIM); + goto out; + } + if (PySequence_Fast_GET_SIZE(seq) != view.ndim) { + PyErr_SetString(PyExc_ValueError, + "get_pointer(): len(indices) != ndim"); + goto out; + } + + for (i = 0; i < view.ndim; i++) { + PyObject *x = PySequence_Fast_GET_ITEM(seq, i); + indices[i] = PyLong_AsSsize_t(x); + if (PyErr_Occurred()) + goto out; + if (indices[i] < 0 || indices[i] >= view.shape[i]) { + PyErr_Format(PyExc_ValueError, + "get_pointer(): invalid index %zd at position %zd", + indices[i], i); + goto out; + } + } + + ptr = PyBuffer_GetPointer(&view, indices); + ret = unpack_single(ptr, view.format, view.itemsize); + +out: + PyBuffer_Release(&view); + return ret; +} + +static char +get_ascii_order(PyObject *order) +{ + PyObject *ascii_order; + char ord; + + if (!PyUnicode_Check(order)) { + PyErr_SetString(PyExc_TypeError, + "order must be a string"); + return CHAR_MAX; + } + + ascii_order = PyUnicode_AsASCIIString(order); + if (ascii_order == NULL) { + return CHAR_MAX; + } + + ord = PyBytes_AS_STRING(ascii_order)[0]; + Py_DECREF(ascii_order); + return ord; +} + +/* Get a contiguous memoryview. */ +static PyObject * +get_contiguous(PyObject *self, PyObject *args) +{ + PyObject *obj; + PyObject *buffertype; + PyObject *order; + long type; + char ord; + + if (!PyArg_ParseTuple(args, "OOO", &obj, &buffertype, &order)) { + return NULL; + } + + if (!PyLong_Check(buffertype)) { + PyErr_SetString(PyExc_TypeError, + "buffertype must be PyBUF_READ or PyBUF_WRITE"); + return NULL; + } + type = PyLong_AsLong(buffertype); + if (type == -1 && PyErr_Occurred()) { + return NULL; + } + + ord = get_ascii_order(order); + if (ord == CHAR_MAX) { + return NULL; + } + + return PyMemoryView_GetContiguous(obj, (int)type, ord); +} + +static int +fmtcmp(const char *fmt1, const char *fmt2) +{ + if (fmt1 == NULL) { + return fmt2 == NULL || strcmp(fmt2, "B") == 0; + } + if (fmt2 == NULL) { + return fmt1 == NULL || strcmp(fmt1, "B") == 0; + } + return strcmp(fmt1, fmt2) == 0; +} + +static int +arraycmp(const Py_ssize_t *a1, const Py_ssize_t *a2, const Py_ssize_t *shape, + Py_ssize_t ndim) +{ + Py_ssize_t i; + + if (ndim == 1 && shape && shape[0] == 1) { + /* This is for comparing strides: For example, the array + [175], shape=[1], strides=[-5] is considered contiguous. */ + return 1; + } + + for (i = 0; i < ndim; i++) { + if (a1[i] != a2[i]) { + return 0; + } + } + + return 1; +} + +/* Compare two contiguous buffers for physical equality. */ +static PyObject * +cmp_contig(PyObject *self, PyObject *args) +{ + PyObject *b1, *b2; /* buffer objects */ + Py_buffer v1, v2; + PyObject *ret; + int equal = 0; + + if (!PyArg_ParseTuple(args, "OO", &b1, &b2)) { + return NULL; + } + + if (PyObject_GetBuffer(b1, &v1, PyBUF_FULL_RO) < 0) { + PyErr_SetString(PyExc_TypeError, + "cmp_contig: first argument does not implement the buffer " + "protocol"); + return NULL; + } + if (PyObject_GetBuffer(b2, &v2, PyBUF_FULL_RO) < 0) { + PyErr_SetString(PyExc_TypeError, + "cmp_contig: second argument does not implement the buffer " + "protocol"); + PyBuffer_Release(&v1); + return NULL; + } + + if (!(PyBuffer_IsContiguous(&v1, 'C')&&PyBuffer_IsContiguous(&v2, 'C')) && + !(PyBuffer_IsContiguous(&v1, 'F')&&PyBuffer_IsContiguous(&v2, 'F'))) { + goto result; + } + + /* readonly may differ if created from non-contiguous */ + if (v1.len != v2.len || + v1.itemsize != v2.itemsize || + v1.ndim != v2.ndim || + !fmtcmp(v1.format, v2.format) || + !!v1.shape != !!v2.shape || + !!v1.strides != !!v2.strides || + !!v1.suboffsets != !!v2.suboffsets) { + goto result; + } + + if ((v1.shape && !arraycmp(v1.shape, v2.shape, NULL, v1.ndim)) || + (v1.strides && !arraycmp(v1.strides, v2.strides, v1.shape, v1.ndim)) || + (v1.suboffsets && !arraycmp(v1.suboffsets, v2.suboffsets, NULL, + v1.ndim))) { + goto result; + } + + if (memcmp((char *)v1.buf, (char *)v2.buf, v1.len) != 0) { + goto result; + } + + equal = 1; + +result: + PyBuffer_Release(&v1); + PyBuffer_Release(&v2); + + ret = equal ? Py_True : Py_False; + Py_INCREF(ret); + return ret; +} + +static PyObject * +is_contiguous(PyObject *self, PyObject *args) +{ + PyObject *obj; + PyObject *order; + PyObject *ret = NULL; + Py_buffer view; + char ord; + + if (!PyArg_ParseTuple(args, "OO", &obj, &order)) { + return NULL; + } + + if (PyObject_GetBuffer(obj, &view, PyBUF_FULL_RO) < 0) { + PyErr_SetString(PyExc_TypeError, + "is_contiguous: object does not implement the buffer " + "protocol"); + return NULL; + } + + ord = get_ascii_order(order); + if (ord == CHAR_MAX) { + goto release; + } + + ret = PyBuffer_IsContiguous(&view, ord) ? Py_True : Py_False; + Py_INCREF(ret); + +release: + PyBuffer_Release(&view); + return ret; +} + +static Py_hash_t +ndarray_hash(PyObject *self) +{ + const NDArrayObject *nd = (NDArrayObject *)self; + const Py_buffer *view = &nd->head->base; + PyObject *bytes; + Py_hash_t hash; + + if (!view->readonly) { + PyErr_SetString(PyExc_ValueError, + "cannot hash writable ndarray object"); + return -1; + } + if (view->obj != NULL && PyObject_Hash(view->obj) == -1) { + return -1; + } + + bytes = ndarray_tobytes(self, NULL); + if (bytes == NULL) { + return -1; + } + + hash = PyObject_Hash(bytes); + Py_DECREF(bytes); + return hash; +} + + +static PyMethodDef ndarray_methods [] = +{ + { "tolist", ndarray_tolist, METH_NOARGS, NULL }, + { "tobytes", ndarray_tobytes, METH_NOARGS, NULL }, + { "push", (PyCFunction)ndarray_push, METH_VARARGS|METH_KEYWORDS, NULL }, + { "pop", ndarray_pop, METH_NOARGS, NULL }, + { "add_suboffsets", ndarray_add_suboffsets, METH_NOARGS, NULL }, + { "memoryview_from_buffer", ndarray_memoryview_from_buffer, METH_NOARGS, NULL }, + {NULL} +}; + +static PyTypeObject NDArray_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "ndarray", /* Name of this type */ + sizeof(NDArrayObject), /* Basic object size */ + 0, /* Item size for varobject */ + (destructor)ndarray_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + &ndarray_as_sequence, /* tp_as_sequence */ + &ndarray_as_mapping, /* tp_as_mapping */ + (hashfunc)ndarray_hash, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + PyObject_GenericGetAttr, /* tp_getattro */ + 0, /* tp_setattro */ + &ndarray_as_buffer, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + 0, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + ndarray_methods, /* tp_methods */ + 0, /* tp_members */ + ndarray_getset, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + ndarray_init, /* tp_init */ + 0, /* tp_alloc */ + ndarray_new, /* tp_new */ +}; + + +static struct PyMethodDef _testbuffer_functions[] = { + {"slice_indices", slice_indices, METH_VARARGS, NULL}, + {"get_pointer", get_pointer, METH_VARARGS, NULL}, + {"get_contiguous", get_contiguous, METH_VARARGS, NULL}, + {"is_contiguous", is_contiguous, METH_VARARGS, NULL}, + {"cmp_contig", cmp_contig, METH_VARARGS, NULL}, + {NULL, NULL} +}; + +static struct PyModuleDef _testbuffermodule = { + PyModuleDef_HEAD_INIT, + "_testbuffer", + NULL, + -1, + _testbuffer_functions, + NULL, + NULL, + NULL, + NULL +}; + + +PyMODINIT_FUNC +PyInit__testbuffer(void) +{ + PyObject *m; + + m = PyModule_Create(&_testbuffermodule); + if (m == NULL) + return NULL; + + Py_TYPE(&NDArray_Type)=&PyType_Type; + Py_INCREF(&NDArray_Type); + PyModule_AddObject(m, "ndarray", (PyObject *)&NDArray_Type); + + structmodule = PyImport_ImportModule("struct"); + if (structmodule == NULL) + return NULL; + + Struct = PyObject_GetAttrString(structmodule, "Struct"); + calcsize = PyObject_GetAttrString(structmodule, "calcsize"); + if (Struct == NULL || calcsize == NULL) + return NULL; + + simple_format = PyUnicode_FromString(simple_fmt); + if (simple_format == NULL) + return NULL; + + PyModule_AddIntConstant(m, "ND_MAX_NDIM", ND_MAX_NDIM); + PyModule_AddIntConstant(m, "ND_VAREXPORT", ND_VAREXPORT); + PyModule_AddIntConstant(m, "ND_WRITABLE", ND_WRITABLE); + PyModule_AddIntConstant(m, "ND_FORTRAN", ND_FORTRAN); + PyModule_AddIntConstant(m, "ND_SCALAR", ND_SCALAR); + PyModule_AddIntConstant(m, "ND_PIL", ND_PIL); + PyModule_AddIntConstant(m, "ND_GETBUF_FAIL", ND_GETBUF_FAIL); + + PyModule_AddIntConstant(m, "PyBUF_SIMPLE", PyBUF_SIMPLE); + PyModule_AddIntConstant(m, "PyBUF_WRITABLE", PyBUF_WRITABLE); + PyModule_AddIntConstant(m, "PyBUF_FORMAT", PyBUF_FORMAT); + PyModule_AddIntConstant(m, "PyBUF_ND", PyBUF_ND); + PyModule_AddIntConstant(m, "PyBUF_STRIDES", PyBUF_STRIDES); + PyModule_AddIntConstant(m, "PyBUF_INDIRECT", PyBUF_INDIRECT); + PyModule_AddIntConstant(m, "PyBUF_C_CONTIGUOUS", PyBUF_C_CONTIGUOUS); + PyModule_AddIntConstant(m, "PyBUF_F_CONTIGUOUS", PyBUF_F_CONTIGUOUS); + PyModule_AddIntConstant(m, "PyBUF_ANY_CONTIGUOUS", PyBUF_ANY_CONTIGUOUS); + PyModule_AddIntConstant(m, "PyBUF_FULL", PyBUF_FULL); + PyModule_AddIntConstant(m, "PyBUF_FULL_RO", PyBUF_FULL_RO); + PyModule_AddIntConstant(m, "PyBUF_RECORDS", PyBUF_RECORDS); + PyModule_AddIntConstant(m, "PyBUF_RECORDS_RO", PyBUF_RECORDS_RO); + PyModule_AddIntConstant(m, "PyBUF_STRIDED", PyBUF_STRIDED); + PyModule_AddIntConstant(m, "PyBUF_STRIDED_RO", PyBUF_STRIDED_RO); + PyModule_AddIntConstant(m, "PyBUF_CONTIG", PyBUF_CONTIG); + PyModule_AddIntConstant(m, "PyBUF_CONTIG_RO", PyBUF_CONTIG_RO); + + PyModule_AddIntConstant(m, "PyBUF_READ", PyBUF_READ); + PyModule_AddIntConstant(m, "PyBUF_WRITE", PyBUF_WRITE); + + return m; +} + + + diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index bcb3a0fcbf2..23a4d5ac9b7 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -275,95 +275,6 @@ test_lazy_hash_inheritance(PyObject* self) } -/* Issue #7385: Check that memoryview() does not crash - * when bf_getbuffer returns an error - */ - -static int -broken_buffer_getbuffer(PyObject *self, Py_buffer *view, int flags) -{ - PyErr_SetString( - TestError, - "test_broken_memoryview: expected error in bf_getbuffer"); - return -1; -} - -static PyBufferProcs memoryviewtester_as_buffer = { - (getbufferproc)broken_buffer_getbuffer, /* bf_getbuffer */ - 0, /* bf_releasebuffer */ -}; - -static PyTypeObject _MemoryViewTester_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "memoryviewtester", /* Name of this type */ - sizeof(PyObject), /* Basic object size */ - 0, /* Item size for varobject */ - (destructor)PyObject_Del, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - PyObject_GenericGetAttr, /* tp_getattro */ - 0, /* tp_setattro */ - &memoryviewtester_as_buffer, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ - 0, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - 0, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - PyType_GenericNew, /* tp_new */ -}; - -static PyObject* -test_broken_memoryview(PyObject* self) -{ - PyObject *obj = PyObject_New(PyObject, &_MemoryViewTester_Type); - PyObject *res; - - if (obj == NULL) { - PyErr_Clear(); - PyErr_SetString( - TestError, - "test_broken_memoryview: failed to create object"); - return NULL; - } - - res = PyMemoryView_FromObject(obj); - if (res || !PyErr_Occurred()){ - PyErr_SetString( - TestError, - "test_broken_memoryview: memoryview() didn't raise an Exception"); - Py_XDECREF(res); - Py_DECREF(obj); - return NULL; - } - - PyErr_Clear(); - Py_DECREF(obj); - Py_RETURN_NONE; -} - - /* Tests of PyLong_{As, From}{Unsigned,}Long(), and (#ifdef HAVE_LONG_LONG) PyLong_{As, From}{Unsigned,}LongLong(). @@ -2421,7 +2332,6 @@ static PyMethodDef TestMethods[] = { {"test_list_api", (PyCFunction)test_list_api, METH_NOARGS}, {"test_dict_iteration", (PyCFunction)test_dict_iteration,METH_NOARGS}, {"test_lazy_hash_inheritance", (PyCFunction)test_lazy_hash_inheritance,METH_NOARGS}, - {"test_broken_memoryview", (PyCFunction)test_broken_memoryview,METH_NOARGS}, {"test_long_api", (PyCFunction)test_long_api, METH_NOARGS}, {"test_long_and_overflow", (PyCFunction)test_long_and_overflow, METH_NOARGS}, @@ -2684,7 +2594,6 @@ PyInit__testcapi(void) return NULL; Py_TYPE(&_HashInheritanceTester_Type)=&PyType_Type; - Py_TYPE(&_MemoryViewTester_Type)=&PyType_Type; Py_TYPE(&test_structmembersType)=&PyType_Type; Py_INCREF(&test_structmembersType); diff --git a/Objects/abstract.c b/Objects/abstract.c index 47010d60e50..62fccdcce63 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -340,7 +340,7 @@ PyObject_GetBuffer(PyObject *obj, Py_buffer *view, int flags) } static int -_IsFortranContiguous(Py_buffer *view) +_IsFortranContiguous(const Py_buffer *view) { Py_ssize_t sd, dim; int i; @@ -361,7 +361,7 @@ _IsFortranContiguous(Py_buffer *view) } static int -_IsCContiguous(Py_buffer *view) +_IsCContiguous(const Py_buffer *view) { Py_ssize_t sd, dim; int i; @@ -382,16 +382,16 @@ _IsCContiguous(Py_buffer *view) } int -PyBuffer_IsContiguous(Py_buffer *view, char fort) +PyBuffer_IsContiguous(const Py_buffer *view, char order) { if (view->suboffsets != NULL) return 0; - if (fort == 'C') + if (order == 'C') return _IsCContiguous(view); - else if (fort == 'F') + else if (order == 'F') return _IsFortranContiguous(view); - else if (fort == 'A') + else if (order == 'A') return (_IsCContiguous(view) || _IsFortranContiguous(view)); return 0; } @@ -651,7 +651,7 @@ int PyBuffer_FillInfo(Py_buffer *view, PyObject *obj, void *buf, Py_ssize_t len, int readonly, int flags) { - if (view == NULL) return 0; + if (view == NULL) return 0; /* XXX why not -1? */ if (((flags & PyBUF_WRITABLE) == PyBUF_WRITABLE) && (readonly == 1)) { PyErr_SetString(PyExc_BufferError, diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 295a742d6d3..e87abf5497c 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -1,127 +1,918 @@ - /* Memoryview object implementation */ #include "Python.h" +#include -#define IS_RELEASED(memobj) \ - (((PyMemoryViewObject *) memobj)->view.buf == NULL) -#define CHECK_RELEASED(memobj) \ - if (IS_RELEASED(memobj)) { \ - PyErr_SetString(PyExc_ValueError, \ - "operation forbidden on released memoryview object"); \ - return NULL; \ +/****************************************************************************/ +/* ManagedBuffer Object */ +/****************************************************************************/ + +/* + ManagedBuffer Object: + --------------------- + + The purpose of this object is to facilitate the handling of chained + memoryviews that have the same underlying exporting object. PEP-3118 + allows the underlying object to change while a view is exported. This + could lead to unexpected results when constructing a new memoryview + from an existing memoryview. + + Rather than repeatedly redirecting buffer requests to the original base + object, all chained memoryviews use a single buffer snapshot. This + snapshot is generated by the constructor _PyManagedBuffer_FromObject(). + + Ownership rules: + ---------------- + + The master buffer inside a managed buffer is filled in by the original + base object. shape, strides, suboffsets and format are read-only for + all consumers. + + A memoryview's buffer is a private copy of the exporter's buffer. shape, + strides and suboffsets belong to the memoryview and are thus writable. + + If a memoryview itself exports several buffers via memory_getbuf(), all + buffer copies share shape, strides and suboffsets. In this case, the + arrays are NOT writable. + + Reference count assumptions: + ---------------------------- + + The 'obj' member of a Py_buffer must either be NULL or refer to the + exporting base object. In the Python codebase, all getbufferprocs + return a new reference to view.obj (example: bytes_buffer_getbuffer()). + + PyBuffer_Release() decrements view.obj (if non-NULL), so the + releasebufferprocs must NOT decrement view.obj. +*/ + + +#define XSTRINGIZE(v) #v +#define STRINGIZE(v) XSTRINGIZE(v) + +#define CHECK_MBUF_RELEASED(mbuf) \ + if (((_PyManagedBufferObject *)mbuf)->flags&_Py_MANAGED_BUFFER_RELEASED) { \ + PyErr_SetString(PyExc_ValueError, \ + "operation forbidden on released memoryview object"); \ + return NULL; \ } -#define CHECK_RELEASED_INT(memobj) \ - if (IS_RELEASED(memobj)) { \ - PyErr_SetString(PyExc_ValueError, \ - "operation forbidden on released memoryview object"); \ - return -1; \ - } -static Py_ssize_t -get_shape0(Py_buffer *buf) +Py_LOCAL_INLINE(_PyManagedBufferObject *) +mbuf_alloc(void) { - if (buf->shape != NULL) - return buf->shape[0]; - if (buf->ndim == 0) - return 1; - PyErr_SetString(PyExc_TypeError, - "exported buffer does not have any shape information associated " - "to it"); - return -1; + _PyManagedBufferObject *mbuf; + + mbuf = (_PyManagedBufferObject *) + PyObject_GC_New(_PyManagedBufferObject, &_PyManagedBuffer_Type); + if (mbuf == NULL) + return NULL; + mbuf->flags = 0; + mbuf->exports = 0; + mbuf->master.obj = NULL; + _PyObject_GC_TRACK(mbuf); + + return mbuf; +} + +static PyObject * +_PyManagedBuffer_FromObject(PyObject *base) +{ + _PyManagedBufferObject *mbuf; + + mbuf = mbuf_alloc(); + if (mbuf == NULL) + return NULL; + + if (PyObject_GetBuffer(base, &mbuf->master, PyBUF_FULL_RO) < 0) { + /* mbuf->master.obj must be NULL. */ + Py_DECREF(mbuf); + return NULL; + } + + /* Assume that master.obj is a new reference to base. */ + assert(mbuf->master.obj == base); + + return (PyObject *)mbuf; } static void -dup_buffer(Py_buffer *dest, Py_buffer *src) +mbuf_release(_PyManagedBufferObject *self) { - *dest = *src; - if (src->ndim == 1 && src->shape != NULL) { - dest->shape = &(dest->smalltable[0]); - dest->shape[0] = get_shape0(src); - } - if (src->ndim == 1 && src->strides != NULL) { - dest->strides = &(dest->smalltable[1]); - dest->strides[0] = src->strides[0]; - } + if (self->flags&_Py_MANAGED_BUFFER_RELEASED) + return; + + /* NOTE: at this point self->exports can still be > 0 if this function + is called from mbuf_clear() to break up a reference cycle. */ + self->flags |= _Py_MANAGED_BUFFER_RELEASED; + + /* PyBuffer_Release() decrements master->obj and sets it to NULL. */ + _PyObject_GC_UNTRACK(self); + PyBuffer_Release(&self->master); +} + +static void +mbuf_dealloc(_PyManagedBufferObject *self) +{ + assert(self->exports == 0); + mbuf_release(self); + if (self->flags&_Py_MANAGED_BUFFER_FREE_FORMAT) + PyMem_Free(self->master.format); + PyObject_GC_Del(self); } static int -memory_getbuf(PyMemoryViewObject *self, Py_buffer *view, int flags) +mbuf_traverse(_PyManagedBufferObject *self, visitproc visit, void *arg) { - int res = 0; - CHECK_RELEASED_INT(self); - if (self->view.obj != NULL) - res = PyObject_GetBuffer(self->view.obj, view, flags); - if (view) - dup_buffer(view, &self->view); - return res; + Py_VISIT(self->master.obj); + return 0; } -static void -memory_releasebuf(PyMemoryViewObject *self, Py_buffer *view) +static int +mbuf_clear(_PyManagedBufferObject *self) { - PyBuffer_Release(view); + assert(self->exports >= 0); + mbuf_release(self); + return 0; } +PyTypeObject _PyManagedBuffer_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) + "managedbuffer", + sizeof(_PyManagedBufferObject), + 0, + (destructor)mbuf_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_reserved */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + PyObject_GenericGetAttr, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ + 0, /* tp_doc */ + (traverseproc)mbuf_traverse, /* tp_traverse */ + (inquiry)mbuf_clear /* tp_clear */ +}; + + +/****************************************************************************/ +/* MemoryView Object */ +/****************************************************************************/ + +/* In the process of breaking reference cycles mbuf_release() can be + called before memory_release(). */ +#define BASE_INACCESSIBLE(mv) \ + (((PyMemoryViewObject *)mv)->flags&_Py_MEMORYVIEW_RELEASED || \ + ((PyMemoryViewObject *)mv)->mbuf->flags&_Py_MANAGED_BUFFER_RELEASED) + +#define CHECK_RELEASED(mv) \ + if (BASE_INACCESSIBLE(mv)) { \ + PyErr_SetString(PyExc_ValueError, \ + "operation forbidden on released memoryview object"); \ + return NULL; \ + } + +#define CHECK_RELEASED_INT(mv) \ + if (BASE_INACCESSIBLE(mv)) { \ + PyErr_SetString(PyExc_ValueError, \ + "operation forbidden on released memoryview object"); \ + return -1; \ + } + +#define CHECK_LIST_OR_TUPLE(v) \ + if (!PyList_Check(v) && !PyTuple_Check(v)) { \ + PyErr_SetString(PyExc_TypeError, \ + #v " must be a list or a tuple"); \ + return NULL; \ + } + +#define VIEW_ADDR(mv) (&((PyMemoryViewObject *)mv)->view) + +/* Check for the presence of suboffsets in the first dimension. */ +#define HAVE_PTR(suboffsets) (suboffsets && suboffsets[0] >= 0) +/* Adjust ptr if suboffsets are present. */ +#define ADJUST_PTR(ptr, suboffsets) \ + (HAVE_PTR(suboffsets) ? *((char**)ptr) + suboffsets[0] : ptr) + +/* Memoryview buffer properties */ +#define MV_C_CONTIGUOUS(flags) (flags&(_Py_MEMORYVIEW_SCALAR|_Py_MEMORYVIEW_C)) +#define MV_F_CONTIGUOUS(flags) \ + (flags&(_Py_MEMORYVIEW_SCALAR|_Py_MEMORYVIEW_FORTRAN)) +#define MV_ANY_CONTIGUOUS(flags) \ + (flags&(_Py_MEMORYVIEW_SCALAR|_Py_MEMORYVIEW_C|_Py_MEMORYVIEW_FORTRAN)) + +/* Fast contiguity test. Caller must ensure suboffsets==NULL and ndim==1. */ +#define MV_CONTIGUOUS_NDIM1(view) \ + ((view)->shape[0] == 1 || (view)->strides[0] == (view)->itemsize) + +/* getbuffer() requests */ +#define REQ_INDIRECT(flags) ((flags&PyBUF_INDIRECT) == PyBUF_INDIRECT) +#define REQ_C_CONTIGUOUS(flags) ((flags&PyBUF_C_CONTIGUOUS) == PyBUF_C_CONTIGUOUS) +#define REQ_F_CONTIGUOUS(flags) ((flags&PyBUF_F_CONTIGUOUS) == PyBUF_F_CONTIGUOUS) +#define REQ_ANY_CONTIGUOUS(flags) ((flags&PyBUF_ANY_CONTIGUOUS) == PyBUF_ANY_CONTIGUOUS) +#define REQ_STRIDES(flags) ((flags&PyBUF_STRIDES) == PyBUF_STRIDES) +#define REQ_SHAPE(flags) ((flags&PyBUF_ND) == PyBUF_ND) +#define REQ_WRITABLE(flags) (flags&PyBUF_WRITABLE) +#define REQ_FORMAT(flags) (flags&PyBUF_FORMAT) + + PyDoc_STRVAR(memory_doc, "memoryview(object)\n\ \n\ Create a new memoryview object which references the given object."); + +/**************************************************************************/ +/* Copy memoryview buffers */ +/**************************************************************************/ + +/* The functions in this section take a source and a destination buffer + with the same logical structure: format, itemsize, ndim and shape + are identical, with ndim > 0. + + NOTE: All buffers are assumed to have PyBUF_FULL information, which + is the case for memoryviews! */ + + +/* Assumptions: ndim >= 1. The macro tests for a corner case that should + perhaps be explicitly forbidden in the PEP. */ +#define HAVE_SUBOFFSETS_IN_LAST_DIM(view) \ + (view->suboffsets && view->suboffsets[dest->ndim-1] >= 0) + +Py_LOCAL_INLINE(int) +last_dim_is_contiguous(Py_buffer *dest, Py_buffer *src) +{ + assert(dest->ndim > 0 && src->ndim > 0); + return (!HAVE_SUBOFFSETS_IN_LAST_DIM(dest) && + !HAVE_SUBOFFSETS_IN_LAST_DIM(src) && + dest->strides[dest->ndim-1] == dest->itemsize && + src->strides[src->ndim-1] == src->itemsize); +} + +/* Check that the logical structure of the destination and source buffers + is identical. */ +static int +cmp_structure(Py_buffer *dest, Py_buffer *src) +{ + const char *dfmt, *sfmt; + int i; + + assert(dest->format && src->format); + dfmt = dest->format[0] == '@' ? dest->format+1 : dest->format; + sfmt = src->format[0] == '@' ? src->format+1 : src->format; + + if (strcmp(dfmt, sfmt) != 0 || + dest->itemsize != src->itemsize || + dest->ndim != src->ndim) { + goto value_error; + } + + for (i = 0; i < dest->ndim; i++) { + if (dest->shape[i] != src->shape[i]) + goto value_error; + if (dest->shape[i] == 0) + break; + } + + return 0; + +value_error: + PyErr_SetString(PyExc_ValueError, + "ndarray assignment: lvalue and rvalue have different structures"); + return -1; +} + +/* Base case for recursive multi-dimensional copying. Contiguous arrays are + copied with very little overhead. Assumptions: ndim == 1, mem == NULL or + sizeof(mem) == shape[0] * itemsize. */ +static void +copy_base(const Py_ssize_t *shape, Py_ssize_t itemsize, + char *dptr, const Py_ssize_t *dstrides, const Py_ssize_t *dsuboffsets, + char *sptr, const Py_ssize_t *sstrides, const Py_ssize_t *ssuboffsets, + char *mem) +{ + if (mem == NULL) { /* contiguous */ + Py_ssize_t size = shape[0] * itemsize; + if (dptr + size < sptr || sptr + size < dptr) + memcpy(dptr, sptr, size); /* no overlapping */ + else + memmove(dptr, sptr, size); + } + else { + char *p; + Py_ssize_t i; + for (i=0, p=mem; i < shape[0]; p+=itemsize, sptr+=sstrides[0], i++) { + char *xsptr = ADJUST_PTR(sptr, ssuboffsets); + memcpy(p, xsptr, itemsize); + } + for (i=0, p=mem; i < shape[0]; p+=itemsize, dptr+=dstrides[0], i++) { + char *xdptr = ADJUST_PTR(dptr, dsuboffsets); + memcpy(xdptr, p, itemsize); + } + } + +} + +/* Recursively copy a source buffer to a destination buffer. The two buffers + have the same ndim, shape and itemsize. */ +static void +copy_rec(const Py_ssize_t *shape, Py_ssize_t ndim, Py_ssize_t itemsize, + char *dptr, const Py_ssize_t *dstrides, const Py_ssize_t *dsuboffsets, + char *sptr, const Py_ssize_t *sstrides, const Py_ssize_t *ssuboffsets, + char *mem) +{ + Py_ssize_t i; + + assert(ndim >= 1); + + if (ndim == 1) { + copy_base(shape, itemsize, + dptr, dstrides, dsuboffsets, + sptr, sstrides, ssuboffsets, + mem); + return; + } + + for (i = 0; i < shape[0]; dptr+=dstrides[0], sptr+=sstrides[0], i++) { + char *xdptr = ADJUST_PTR(dptr, dsuboffsets); + char *xsptr = ADJUST_PTR(sptr, ssuboffsets); + + copy_rec(shape+1, ndim-1, itemsize, + xdptr, dstrides+1, dsuboffsets ? dsuboffsets+1 : NULL, + xsptr, sstrides+1, ssuboffsets ? ssuboffsets+1 : NULL, + mem); + } +} + +/* Faster copying of one-dimensional arrays. */ +static int +copy_single(Py_buffer *dest, Py_buffer *src) +{ + char *mem = NULL; + + assert(dest->ndim == 1); + + if (cmp_structure(dest, src) < 0) + return -1; + + if (!last_dim_is_contiguous(dest, src)) { + mem = PyMem_Malloc(dest->shape[0] * dest->itemsize); + if (mem == NULL) { + PyErr_NoMemory(); + return -1; + } + } + + copy_base(dest->shape, dest->itemsize, + dest->buf, dest->strides, dest->suboffsets, + src->buf, src->strides, src->suboffsets, + mem); + + if (mem) + PyMem_Free(mem); + + return 0; +} + +/* Recursively copy src to dest. Both buffers must have the same basic + structure. Copying is atomic, the function never fails with a partial + copy. */ +static int +copy_buffer(Py_buffer *dest, Py_buffer *src) +{ + char *mem = NULL; + + assert(dest->ndim > 0); + + if (cmp_structure(dest, src) < 0) + return -1; + + if (!last_dim_is_contiguous(dest, src)) { + mem = PyMem_Malloc(dest->shape[dest->ndim-1] * dest->itemsize); + if (mem == NULL) { + PyErr_NoMemory(); + return -1; + } + } + + copy_rec(dest->shape, dest->ndim, dest->itemsize, + dest->buf, dest->strides, dest->suboffsets, + src->buf, src->strides, src->suboffsets, + mem); + + if (mem) + PyMem_Free(mem); + + return 0; +} + +/* Initialize strides for a C-contiguous array. */ +Py_LOCAL_INLINE(void) +init_strides_from_shape(Py_buffer *view) +{ + Py_ssize_t i; + + assert(view->ndim > 0); + + view->strides[view->ndim-1] = view->itemsize; + for (i = view->ndim-2; i >= 0; i--) + view->strides[i] = view->strides[i+1] * view->shape[i+1]; +} + +/* Initialize strides for a Fortran-contiguous array. */ +Py_LOCAL_INLINE(void) +init_fortran_strides_from_shape(Py_buffer *view) +{ + Py_ssize_t i; + + assert(view->ndim > 0); + + view->strides[0] = view->itemsize; + for (i = 1; i < view->ndim; i++) + view->strides[i] = view->strides[i-1] * view->shape[i-1]; +} + +/* Copy src to a C-contiguous representation. Assumptions: + len(mem) == src->len. */ +static int +buffer_to_c_contiguous(char *mem, Py_buffer *src) +{ + Py_buffer dest; + Py_ssize_t *strides; + int ret; + + assert(src->shape != NULL); + assert(src->strides != NULL); + + strides = PyMem_Malloc(src->ndim * (sizeof *src->strides)); + if (strides == NULL) { + PyErr_NoMemory(); + return -1; + } + + /* initialize dest as a C-contiguous buffer */ + dest = *src; + dest.buf = mem; + /* shape is constant and shared */ + dest.strides = strides; + init_strides_from_shape(&dest); + dest.suboffsets = NULL; + + ret = copy_buffer(&dest, src); + + PyMem_Free(strides); + return ret; +} + + +/****************************************************************************/ +/* Constructors */ +/****************************************************************************/ + +/* Initialize values that are shared with the managed buffer. */ +Py_LOCAL_INLINE(void) +init_shared_values(Py_buffer *dest, const Py_buffer *src) +{ + dest->obj = src->obj; + dest->buf = src->buf; + dest->len = src->len; + dest->itemsize = src->itemsize; + dest->readonly = src->readonly; + dest->format = src->format ? src->format : "B"; + dest->internal = src->internal; +} + +/* Copy shape and strides. Reconstruct missing values. */ +static void +init_shape_strides(Py_buffer *dest, const Py_buffer *src) +{ + Py_ssize_t i; + + if (src->ndim == 0) { + dest->shape = NULL; + dest->strides = NULL; + return; + } + if (src->ndim == 1) { + dest->shape[0] = src->shape ? src->shape[0] : src->len / src->itemsize; + dest->strides[0] = src->strides ? src->strides[0] : src->itemsize; + return; + } + + for (i = 0; i < src->ndim; i++) + dest->shape[i] = src->shape[i]; + if (src->strides) { + for (i = 0; i < src->ndim; i++) + dest->strides[i] = src->strides[i]; + } + else { + init_strides_from_shape(dest); + } +} + +Py_LOCAL_INLINE(void) +init_suboffsets(Py_buffer *dest, const Py_buffer *src) +{ + Py_ssize_t i; + + if (src->suboffsets == NULL) { + dest->suboffsets = NULL; + return; + } + for (i = 0; i < src->ndim; i++) + dest->suboffsets[i] = src->suboffsets[i]; +} + +/* len = product(shape) * itemsize */ +Py_LOCAL_INLINE(void) +init_len(Py_buffer *view) +{ + Py_ssize_t i, len; + + len = 1; + for (i = 0; i < view->ndim; i++) + len *= view->shape[i]; + len *= view->itemsize; + + view->len = len; +} + +/* Initialize memoryview buffer properties. */ +static void +init_flags(PyMemoryViewObject *mv) +{ + const Py_buffer *view = &mv->view; + int flags = 0; + + switch (view->ndim) { + case 0: + flags |= (_Py_MEMORYVIEW_SCALAR|_Py_MEMORYVIEW_C| + _Py_MEMORYVIEW_FORTRAN); + break; + case 1: + if (MV_CONTIGUOUS_NDIM1(view)) + flags |= (_Py_MEMORYVIEW_C|_Py_MEMORYVIEW_FORTRAN); + break; + default: + if (PyBuffer_IsContiguous(view, 'C')) + flags |= _Py_MEMORYVIEW_C; + if (PyBuffer_IsContiguous(view, 'F')) + flags |= _Py_MEMORYVIEW_FORTRAN; + break; + } + + if (view->suboffsets) { + flags |= _Py_MEMORYVIEW_PIL; + flags &= ~(_Py_MEMORYVIEW_C|_Py_MEMORYVIEW_FORTRAN); + } + + mv->flags = flags; +} + +/* Allocate a new memoryview and perform basic initialization. New memoryviews + are exclusively created through the mbuf_add functions. */ +Py_LOCAL_INLINE(PyMemoryViewObject *) +memory_alloc(int ndim) +{ + PyMemoryViewObject *mv; + + mv = (PyMemoryViewObject *) + PyObject_GC_NewVar(PyMemoryViewObject, &PyMemoryView_Type, 3*ndim); + if (mv == NULL) + return NULL; + + mv->mbuf = NULL; + mv->hash = -1; + mv->flags = 0; + mv->exports = 0; + mv->view.ndim = ndim; + mv->view.shape = mv->ob_array; + mv->view.strides = mv->ob_array + ndim; + mv->view.suboffsets = mv->ob_array + 2 * ndim; + + _PyObject_GC_TRACK(mv); + return mv; +} + +/* + Return a new memoryview that is registered with mbuf. If src is NULL, + use mbuf->master as the underlying buffer. Otherwise, use src. + + The new memoryview has full buffer information: shape and strides + are always present, suboffsets as needed. Arrays are copied to + the memoryview's ob_array field. + */ +static PyObject * +mbuf_add_view(_PyManagedBufferObject *mbuf, const Py_buffer *src) +{ + PyMemoryViewObject *mv; + Py_buffer *dest; + + if (src == NULL) + src = &mbuf->master; + + if (src->ndim > PyBUF_MAX_NDIM) { + PyErr_SetString(PyExc_ValueError, + "memoryview: number of dimensions must not exceed " + STRINGIZE(PyBUF_MAX_NDIM)); + return NULL; + } + + mv = memory_alloc(src->ndim); + if (mv == NULL) + return NULL; + + dest = &mv->view; + init_shared_values(dest, src); + init_shape_strides(dest, src); + init_suboffsets(dest, src); + init_flags(mv); + + mv->mbuf = mbuf; + Py_INCREF(mbuf); + mbuf->exports++; + + return (PyObject *)mv; +} + +/* Register an incomplete view: shape, strides, suboffsets and flags still + need to be initialized. Use 'ndim' instead of src->ndim to determine the + size of the memoryview's ob_array. + + Assumption: ndim <= PyBUF_MAX_NDIM. */ +static PyObject * +mbuf_add_incomplete_view(_PyManagedBufferObject *mbuf, const Py_buffer *src, + int ndim) +{ + PyMemoryViewObject *mv; + Py_buffer *dest; + + if (src == NULL) + src = &mbuf->master; + + assert(ndim <= PyBUF_MAX_NDIM); + + mv = memory_alloc(ndim); + if (mv == NULL) + return NULL; + + dest = &mv->view; + init_shared_values(dest, src); + + mv->mbuf = mbuf; + Py_INCREF(mbuf); + mbuf->exports++; + + return (PyObject *)mv; +} + +/* Expose a raw memory area as a view of contiguous bytes. flags can be + PyBUF_READ or PyBUF_WRITE. view->format is set to "B" (unsigned bytes). + The memoryview has complete buffer information. */ +PyObject * +PyMemoryView_FromMemory(char *mem, Py_ssize_t size, int flags) +{ + _PyManagedBufferObject *mbuf; + PyObject *mv; + int readonly; + + assert(mem != NULL); + assert(flags == PyBUF_READ || flags == PyBUF_WRITE); + + mbuf = mbuf_alloc(); + if (mbuf == NULL) + return NULL; + + readonly = (flags == PyBUF_WRITE) ? 0 : 1; + (void)PyBuffer_FillInfo(&mbuf->master, NULL, mem, size, readonly, + PyBUF_FULL_RO); + + mv = mbuf_add_view(mbuf, NULL); + Py_DECREF(mbuf); + + return mv; +} + +/* Create a memoryview from a given Py_buffer. For simple byte views, + PyMemoryView_FromMemory() should be used instead. + This function is the only entry point that can create a master buffer + without full information. Because of this fact init_shape_strides() + must be able to reconstruct missing values. */ PyObject * PyMemoryView_FromBuffer(Py_buffer *info) { - PyMemoryViewObject *mview; + _PyManagedBufferObject *mbuf; + PyObject *mv; if (info->buf == NULL) { PyErr_SetString(PyExc_ValueError, - "cannot make memory view from a buffer with a NULL data pointer"); + "PyMemoryView_FromBuffer(): info->buf must not be NULL"); return NULL; } - mview = (PyMemoryViewObject *) - PyObject_GC_New(PyMemoryViewObject, &PyMemoryView_Type); - if (mview == NULL) + + mbuf = mbuf_alloc(); + if (mbuf == NULL) return NULL; - mview->hash = -1; - dup_buffer(&mview->view, info); - /* NOTE: mview->view.obj should already have been incref'ed as - part of PyBuffer_FillInfo(). */ - _PyObject_GC_TRACK(mview); - return (PyObject *)mview; + + /* info->obj is either NULL or a borrowed reference. This reference + should not be decremented in PyBuffer_Release(). */ + mbuf->master = *info; + mbuf->master.obj = NULL; + + mv = mbuf_add_view(mbuf, NULL); + Py_DECREF(mbuf); + + return mv; } +/* Create a memoryview from an object that implements the buffer protocol. + If the object is a memoryview, the new memoryview must be registered + with the same managed buffer. Otherwise, a new managed buffer is created. */ PyObject * -PyMemoryView_FromObject(PyObject *base) +PyMemoryView_FromObject(PyObject *v) { - PyMemoryViewObject *mview; - Py_buffer view; + _PyManagedBufferObject *mbuf; - if (!PyObject_CheckBuffer(base)) { - PyErr_SetString(PyExc_TypeError, - "cannot make memory view because object does " - "not have the buffer interface"); - return NULL; + if (PyMemoryView_Check(v)) { + PyMemoryViewObject *mv = (PyMemoryViewObject *)v; + CHECK_RELEASED(mv); + return mbuf_add_view(mv->mbuf, &mv->view); + } + else if (PyObject_CheckBuffer(v)) { + PyObject *ret; + mbuf = (_PyManagedBufferObject *)_PyManagedBuffer_FromObject(v); + if (mbuf == NULL) + return NULL; + ret = mbuf_add_view(mbuf, NULL); + Py_DECREF(mbuf); + return ret; } - if (PyObject_GetBuffer(base, &view, PyBUF_FULL_RO) < 0) - return NULL; - - mview = (PyMemoryViewObject *)PyMemoryView_FromBuffer(&view); - if (mview == NULL) { - PyBuffer_Release(&view); - return NULL; - } - - return (PyObject *)mview; + PyErr_Format(PyExc_TypeError, + "memoryview: %.200s object does not have the buffer interface", + Py_TYPE(v)->tp_name); + return NULL; } +/* Copy the format string from a base object that might vanish. */ +static int +mbuf_copy_format(_PyManagedBufferObject *mbuf, const char *fmt) +{ + if (fmt != NULL) { + char *cp = PyMem_Malloc(strlen(fmt)+1); + if (cp == NULL) { + PyErr_NoMemory(); + return -1; + } + mbuf->master.format = strcpy(cp, fmt); + mbuf->flags |= _Py_MANAGED_BUFFER_FREE_FORMAT; + } + + return 0; +} + +/* + Return a memoryview that is based on a contiguous copy of src. + Assumptions: src has PyBUF_FULL_RO information, src->ndim > 0. + + Ownership rules: + 1) As usual, the returned memoryview has a private copy + of src->shape, src->strides and src->suboffsets. + 2) src->format is copied to the master buffer and released + in mbuf_dealloc(). The releasebufferproc of the bytes + object is NULL, so it does not matter that mbuf_release() + passes the altered format pointer to PyBuffer_Release(). +*/ +static PyObject * +memory_from_contiguous_copy(Py_buffer *src, char order) +{ + _PyManagedBufferObject *mbuf; + PyMemoryViewObject *mv; + PyObject *bytes; + Py_buffer *dest; + int i; + + assert(src->ndim > 0); + assert(src->shape != NULL); + + bytes = PyBytes_FromStringAndSize(NULL, src->len); + if (bytes == NULL) + return NULL; + + mbuf = (_PyManagedBufferObject *)_PyManagedBuffer_FromObject(bytes); + Py_DECREF(bytes); + if (mbuf == NULL) + return NULL; + + if (mbuf_copy_format(mbuf, src->format) < 0) { + Py_DECREF(mbuf); + return NULL; + } + + mv = (PyMemoryViewObject *)mbuf_add_incomplete_view(mbuf, NULL, src->ndim); + Py_DECREF(mbuf); + if (mv == NULL) + return NULL; + + dest = &mv->view; + + /* shared values are initialized correctly except for itemsize */ + dest->itemsize = src->itemsize; + + /* shape and strides */ + for (i = 0; i < src->ndim; i++) { + dest->shape[i] = src->shape[i]; + } + if (order == 'C' || order == 'A') { + init_strides_from_shape(dest); + } + else { + init_fortran_strides_from_shape(dest); + } + /* suboffsets */ + dest->suboffsets = NULL; + + /* flags */ + init_flags(mv); + + if (copy_buffer(dest, src) < 0) { + Py_DECREF(mv); + return NULL; + } + + return (PyObject *)mv; +} + +/* + Return a new memoryview object based on a contiguous exporter with + buffertype={PyBUF_READ, PyBUF_WRITE} and order={'C', 'F'ortran, or 'A'ny}. + The logical structure of the input and output buffers is the same + (i.e. tolist(input) == tolist(output)), but the physical layout in + memory can be explicitly chosen. + + As usual, if buffertype=PyBUF_WRITE, the exporter's buffer must be writable, + otherwise it may be writable or read-only. + + If the exporter is already contiguous with the desired target order, + the memoryview will be directly based on the exporter. + + Otherwise, if the buffertype is PyBUF_READ, the memoryview will be + based on a new bytes object. If order={'C', 'A'ny}, use 'C' order, + 'F'ortran order otherwise. +*/ +PyObject * +PyMemoryView_GetContiguous(PyObject *obj, int buffertype, char order) +{ + PyMemoryViewObject *mv; + PyObject *ret; + Py_buffer *view; + + assert(buffertype == PyBUF_READ || buffertype == PyBUF_WRITE); + assert(order == 'C' || order == 'F' || order == 'A'); + + mv = (PyMemoryViewObject *)PyMemoryView_FromObject(obj); + if (mv == NULL) + return NULL; + + view = &mv->view; + if (buffertype == PyBUF_WRITE && view->readonly) { + PyErr_SetString(PyExc_BufferError, + "underlying buffer is not writable"); + Py_DECREF(mv); + return NULL; + } + + if (PyBuffer_IsContiguous(view, order)) + return (PyObject *)mv; + + if (buffertype == PyBUF_WRITE) { + PyErr_SetString(PyExc_BufferError, + "writable contiguous buffer requested " + "for a non-contiguous object."); + Py_DECREF(mv); + return NULL; + } + + ret = memory_from_contiguous_copy(view, order); + Py_DECREF(mv); + return ret; +} + + static PyObject * memory_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds) { PyObject *obj; - static char *kwlist[] = {"object", 0}; + static char *kwlist[] = {"object", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:memoryview", kwlist, &obj)) { @@ -132,203 +923,1571 @@ memory_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds) } -static void -_strided_copy_nd(char *dest, char *src, int nd, Py_ssize_t *shape, - Py_ssize_t *strides, Py_ssize_t itemsize, char fort) +/****************************************************************************/ +/* Release/GC management */ +/****************************************************************************/ + +/* Inform the managed buffer that this particular memoryview will not access + the underlying buffer again. If no other memoryviews are registered with + the managed buffer, the underlying buffer is released instantly and + marked as inaccessible for both the memoryview and the managed buffer. + + This function fails if the memoryview itself has exported buffers. */ +static int +_memory_release(PyMemoryViewObject *self) { - int k; - Py_ssize_t outstride; + if (self->flags & _Py_MEMORYVIEW_RELEASED) + return 0; - if (nd==0) { - memcpy(dest, src, itemsize); + if (self->exports == 0) { + self->flags |= _Py_MEMORYVIEW_RELEASED; + assert(self->mbuf->exports > 0); + if (--self->mbuf->exports == 0) + mbuf_release(self->mbuf); + return 0; } - else if (nd == 1) { - for (k = 0; kexports > 0) { + PyErr_Format(PyExc_BufferError, + "memoryview has %zd exported buffer%s", self->exports, + self->exports==1 ? "" : "s"); + return -1; } - else { - if (fort == 'F') { - /* Copy first dimension first, - second dimension second, etc... - Set up the recursive loop backwards so that final - dimension is actually copied last. - */ - outstride = itemsize; - for (k=1; kexports == 0); + _PyObject_GC_UNTRACK(self); + (void)_memory_release(self); + Py_CLEAR(self->mbuf); + PyObject_GC_Del(self); } static int -_indirect_copy_nd(char *dest, Py_buffer *view, char fort) +memory_traverse(PyMemoryViewObject *self, visitproc visit, void *arg) { - Py_ssize_t *indices; - int k; - Py_ssize_t elements; - char *ptr; - void (*func)(int, Py_ssize_t *, const Py_ssize_t *); + Py_VISIT(self->mbuf); + return 0; +} - if (view->ndim > PY_SSIZE_T_MAX / sizeof(Py_ssize_t)) { - PyErr_NoMemory(); - return -1; +static int +memory_clear(PyMemoryViewObject *self) +{ + (void)_memory_release(self); + Py_CLEAR(self->mbuf); + return 0; +} + +static PyObject * +memory_enter(PyObject *self, PyObject *args) +{ + CHECK_RELEASED(self); + Py_INCREF(self); + return self; +} + +static PyObject * +memory_exit(PyObject *self, PyObject *args) +{ + return memory_release((PyMemoryViewObject *)self); +} + + +/****************************************************************************/ +/* Casting format and shape */ +/****************************************************************************/ + +#define IS_BYTE_FORMAT(f) (f == 'b' || f == 'B' || f == 'c') + +Py_LOCAL_INLINE(Py_ssize_t) +get_native_fmtchar(char *result, const char *fmt) +{ + Py_ssize_t size = -1; + + if (fmt[0] == '@') fmt++; + + switch (fmt[0]) { + case 'c': case 'b': case 'B': size = sizeof(char); break; + case 'h': case 'H': size = sizeof(short); break; + case 'i': case 'I': size = sizeof(int); break; + case 'l': case 'L': size = sizeof(long); break; + #ifdef HAVE_LONG_LONG + case 'q': case 'Q': size = sizeof(PY_LONG_LONG); break; + #endif + case 'n': case 'N': size = sizeof(Py_ssize_t); break; + case 'f': size = sizeof(float); break; + case 'd': size = sizeof(double); break; + #ifdef HAVE_C99_BOOL + case '?': size = sizeof(_Bool); break; + #else + case '?': size = sizeof(char); break; + #endif + case 'P': size = sizeof(void *); break; } - indices = (Py_ssize_t *)PyMem_Malloc(sizeof(Py_ssize_t)*view->ndim); - if (indices == NULL) { - PyErr_NoMemory(); - return -1; - } - for (k=0; kndim;k++) { - indices[k] = 0; + if (size > 0 && fmt[1] == '\0') { + *result = fmt[0]; + return size; } - elements = 1; - for (k=0; kndim; k++) { - elements *= view->shape[k]; + return -1; +} + +/* Cast a memoryview's data type to 'format'. The input array must be + C-contiguous. At least one of input-format, output-format must have + byte size. The output array is 1-D, with the same byte length as the + input array. Thus, view->len must be a multiple of the new itemsize. */ +static int +cast_to_1D(PyMemoryViewObject *mv, PyObject *format) +{ + Py_buffer *view = &mv->view; + PyObject *asciifmt; + char srcchar, destchar; + Py_ssize_t itemsize; + int ret = -1; + + assert(view->ndim >= 1); + assert(Py_SIZE(mv) == 3*view->ndim); + assert(view->shape == mv->ob_array); + assert(view->strides == mv->ob_array + view->ndim); + assert(view->suboffsets == mv->ob_array + 2*view->ndim); + + if (get_native_fmtchar(&srcchar, view->format) < 0) { + PyErr_SetString(PyExc_ValueError, + "memoryview: source format must be a native single character " + "format prefixed with an optional '@'"); + return ret; } - if (fort == 'F') { - func = _Py_add_one_to_index_F; + + asciifmt = PyUnicode_AsASCIIString(format); + if (asciifmt == NULL) + return ret; + + itemsize = get_native_fmtchar(&destchar, PyBytes_AS_STRING(asciifmt)); + if (itemsize < 0) { + PyErr_SetString(PyExc_ValueError, + "memoryview: destination format must be a native single " + "character format prefixed with an optional '@'"); + goto out; + } + + if (!IS_BYTE_FORMAT(srcchar) && !IS_BYTE_FORMAT(destchar)) { + PyErr_SetString(PyExc_TypeError, + "memoryview: cannot cast between two non-byte formats"); + goto out; + } + if (view->len % itemsize) { + PyErr_SetString(PyExc_TypeError, + "memoryview: length is not a multiple of itemsize"); + goto out; + } + + strncpy(mv->format, PyBytes_AS_STRING(asciifmt), + _Py_MEMORYVIEW_MAX_FORMAT); + mv->format[_Py_MEMORYVIEW_MAX_FORMAT-1] = '\0'; + view->format = mv->format; + view->itemsize = itemsize; + + view->ndim = 1; + view->shape[0] = view->len / view->itemsize; + view->strides[0] = view->itemsize; + view->suboffsets = NULL; + + init_flags(mv); + + ret = 0; + +out: + Py_DECREF(asciifmt); + return ret; +} + +/* The memoryview must have space for 3*len(seq) elements. */ +static Py_ssize_t +copy_shape(Py_ssize_t *shape, const PyObject *seq, Py_ssize_t ndim, + Py_ssize_t itemsize) +{ + Py_ssize_t x, i; + Py_ssize_t len = itemsize; + + for (i = 0; i < ndim; i++) { + PyObject *tmp = PySequence_Fast_GET_ITEM(seq, i); + if (!PyLong_Check(tmp)) { + PyErr_SetString(PyExc_TypeError, + "memoryview.cast(): elements of shape must be integers"); + return -1; + } + x = PyLong_AsSsize_t(tmp); + if (x == -1 && PyErr_Occurred()) { + return -1; + } + if (x <= 0) { + /* In general elements of shape may be 0, but not for casting. */ + PyErr_Format(PyExc_ValueError, + "memoryview.cast(): elements of shape must be integers > 0"); + return -1; + } + if (x > PY_SSIZE_T_MAX / len) { + PyErr_Format(PyExc_ValueError, + "memoryview.cast(): product(shape) > SSIZE_MAX"); + return -1; + } + len *= x; + shape[i] = x; + } + + return len; +} + +/* Cast a 1-D array to a new shape. The result array will be C-contiguous. + If the result array does not have exactly the same byte length as the + input array, raise ValueError. */ +static int +cast_to_ND(PyMemoryViewObject *mv, const PyObject *shape, int ndim) +{ + Py_buffer *view = &mv->view; + Py_ssize_t len; + + assert(view->ndim == 1); /* ndim from cast_to_1D() */ + assert(Py_SIZE(mv) == 3*(ndim==0?1:ndim)); /* ndim of result array */ + assert(view->shape == mv->ob_array); + assert(view->strides == mv->ob_array + (ndim==0?1:ndim)); + assert(view->suboffsets == NULL); + + view->ndim = ndim; + if (view->ndim == 0) { + view->shape = NULL; + view->strides = NULL; + len = view->itemsize; } else { - func = _Py_add_one_to_index_C; - } - while (elements--) { - func(view->ndim, indices, view->shape); - ptr = PyBuffer_GetPointer(view, indices); - memcpy(dest, ptr, view->itemsize); - dest += view->itemsize; + len = copy_shape(view->shape, shape, ndim, view->itemsize); + if (len < 0) + return -1; + init_strides_from_shape(view); } - PyMem_Free(indices); + if (view->len != len) { + PyErr_SetString(PyExc_TypeError, + "memoryview: product(shape) * itemsize != buffer size"); + return -1; + } + + init_flags(mv); + + return 0; +} + +static int +zero_in_shape(PyMemoryViewObject *mv) +{ + Py_buffer *view = &mv->view; + Py_ssize_t i; + + for (i = 0; i < view->ndim; i++) + if (view->shape[i] == 0) + return 1; + return 0; } /* - Get a the data from an object as a contiguous chunk of memory (in - either 'C' or 'F'ortran order) even if it means copying it into a - separate memory area. + Cast a copy of 'self' to a different view. The input view must + be C-contiguous. The function always casts the input view to a + 1-D output according to 'format'. At least one of input-format, + output-format must have byte size. - Returns a new reference to a Memory view object. If no copy is needed, - the memory view object points to the original memory and holds a - lock on the original. If a copy is needed, then the memory view object - points to a brand-new Bytes object (and holds a memory lock on it). + If 'shape' is given, the 1-D view from the previous step will + be cast to a C-contiguous view with new shape and strides. - buffertype - - PyBUF_READ buffer only needs to be read-only - PyBUF_WRITE buffer needs to be writable (give error if not contiguous) - PyBUF_SHADOW buffer needs to be writable so shadow it with - a contiguous buffer if it is not. The view will point to - the shadow buffer which can be written to and then - will be copied back into the other buffer when the memory - view is de-allocated. While the shadow buffer is - being used, it will have an exclusive write lock on - the original buffer. - */ - -PyObject * -PyMemoryView_GetContiguous(PyObject *obj, int buffertype, char fort) + All casts must result in views that will have the exact byte + size of the original input. Otherwise, an error is raised. +*/ +static PyObject * +memory_cast(PyMemoryViewObject *self, PyObject *args, PyObject *kwds) { - PyMemoryViewObject *mem; - PyObject *bytes; - Py_buffer *view; - int flags; - char *dest; + static char *kwlist[] = {"format", "shape", NULL}; + PyMemoryViewObject *mv = NULL; + PyObject *shape = NULL; + PyObject *format; + Py_ssize_t ndim = 1; - if (!PyObject_CheckBuffer(obj)) { + CHECK_RELEASED(self); + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, + &format, &shape)) { + return NULL; + } + if (!PyUnicode_Check(format)) { PyErr_SetString(PyExc_TypeError, - "object does not support the buffer interface"); + "memoryview: format argument must be a string"); return NULL; } - - mem = PyObject_GC_New(PyMemoryViewObject, &PyMemoryView_Type); - if (mem == NULL) - return NULL; - - view = &mem->view; - flags = PyBUF_FULL_RO; - switch(buffertype) { - case PyBUF_WRITE: - flags = PyBUF_FULL; - break; - } - - if (PyObject_GetBuffer(obj, view, flags) != 0) { - Py_DECREF(mem); + if (!MV_C_CONTIGUOUS(self->flags)) { + PyErr_SetString(PyExc_TypeError, + "memoryview: casts are restricted to C-contiguous views"); return NULL; } - - if (PyBuffer_IsContiguous(view, fort)) { - /* no copy needed */ - _PyObject_GC_TRACK(mem); - return (PyObject *)mem; - } - /* otherwise a copy is needed */ - if (buffertype == PyBUF_WRITE) { - Py_DECREF(mem); - PyErr_SetString(PyExc_BufferError, - "writable contiguous buffer requested " - "for a non-contiguousobject."); + if (zero_in_shape(self)) { + PyErr_SetString(PyExc_TypeError, + "memoryview: cannot cast view with zeros in shape or strides"); return NULL; } - bytes = PyBytes_FromStringAndSize(NULL, view->len); - if (bytes == NULL) { - Py_DECREF(mem); - return NULL; - } - dest = PyBytes_AS_STRING(bytes); - /* different copying strategy depending on whether - or not any pointer de-referencing is needed - */ - /* strided or in-direct copy */ - if (view->suboffsets==NULL) { - _strided_copy_nd(dest, view->buf, view->ndim, view->shape, - view->strides, view->itemsize, fort); - } - else { - if (_indirect_copy_nd(dest, view, fort) < 0) { - Py_DECREF(bytes); - Py_DECREF(mem); + if (shape) { + CHECK_LIST_OR_TUPLE(shape) + ndim = PySequence_Fast_GET_SIZE(shape); + if (ndim > PyBUF_MAX_NDIM) { + PyErr_SetString(PyExc_ValueError, + "memoryview: number of dimensions must not exceed " + STRINGIZE(PyBUF_MAX_NDIM)); + return NULL; + } + if (self->view.ndim != 1 && ndim != 1) { + PyErr_SetString(PyExc_TypeError, + "memoryview: cast must be 1D -> ND or ND -> 1D"); return NULL; } - PyBuffer_Release(view); /* XXX ? */ } - _PyObject_GC_TRACK(mem); - return (PyObject *)mem; + + mv = (PyMemoryViewObject *) + mbuf_add_incomplete_view(self->mbuf, &self->view, ndim==0 ? 1 : (int)ndim); + if (mv == NULL) + return NULL; + + if (cast_to_1D(mv, format) < 0) + goto error; + if (shape && cast_to_ND(mv, shape, (int)ndim) < 0) + goto error; + + return (PyObject *)mv; + +error: + Py_DECREF(mv); + return NULL; } +/**************************************************************************/ +/* getbuffer */ +/**************************************************************************/ + +static int +memory_getbuf(PyMemoryViewObject *self, Py_buffer *view, int flags) +{ + Py_buffer *base = &self->view; + int baseflags = self->flags; + + CHECK_RELEASED_INT(self); + + /* start with complete information */ + *view = *base; + view->obj = NULL; + + if (REQ_WRITABLE(flags) && base->readonly) { + PyErr_SetString(PyExc_BufferError, + "memoryview: underlying buffer is not writable"); + return -1; + } + if (!REQ_FORMAT(flags)) { + /* NULL indicates that the buffer's data type has been cast to 'B'. + view->itemsize is the _previous_ itemsize. If shape is present, + the equality product(shape) * itemsize = len still holds at this + point. The equality calcsize(format) = itemsize does _not_ hold + from here on! */ + view->format = NULL; + } + + if (REQ_C_CONTIGUOUS(flags) && !MV_C_CONTIGUOUS(baseflags)) { + PyErr_SetString(PyExc_BufferError, + "memoryview: underlying buffer is not C-contiguous"); + return -1; + } + if (REQ_F_CONTIGUOUS(flags) && !MV_F_CONTIGUOUS(baseflags)) { + PyErr_SetString(PyExc_BufferError, + "memoryview: underlying buffer is not Fortran contiguous"); + return -1; + } + if (REQ_ANY_CONTIGUOUS(flags) && !MV_ANY_CONTIGUOUS(baseflags)) { + PyErr_SetString(PyExc_BufferError, + "memoryview: underlying buffer is not contiguous"); + return -1; + } + if (!REQ_INDIRECT(flags) && (baseflags & _Py_MEMORYVIEW_PIL)) { + PyErr_SetString(PyExc_BufferError, + "memoryview: underlying buffer requires suboffsets"); + return -1; + } + if (!REQ_STRIDES(flags)) { + if (!MV_C_CONTIGUOUS(baseflags)) { + PyErr_SetString(PyExc_BufferError, + "memoryview: underlying buffer is not C-contiguous"); + return -1; + } + view->strides = NULL; + } + if (!REQ_SHAPE(flags)) { + /* PyBUF_SIMPLE or PyBUF_WRITABLE: at this point buf is C-contiguous, + so base->buf = ndbuf->data. */ + if (view->format != NULL) { + /* PyBUF_SIMPLE|PyBUF_FORMAT and PyBUF_WRITABLE|PyBUF_FORMAT do + not make sense. */ + PyErr_Format(PyExc_BufferError, + "ndarray: cannot cast to unsigned bytes if the format flag " + "is present"); + return -1; + } + /* product(shape) * itemsize = len and calcsize(format) = itemsize + do _not_ hold from here on! */ + view->ndim = 1; + view->shape = NULL; + } + + + view->obj = (PyObject *)self; + Py_INCREF(view->obj); + self->exports++; + + return 0; +} + +static void +memory_releasebuf(PyMemoryViewObject *self, Py_buffer *view) +{ + self->exports--; + return; + /* PyBuffer_Release() decrements view->obj after this function returns. */ +} + +/* Buffer methods */ +static PyBufferProcs memory_as_buffer = { + (getbufferproc)memory_getbuf, /* bf_getbuffer */ + (releasebufferproc)memory_releasebuf, /* bf_releasebuffer */ +}; + + +/****************************************************************************/ +/* Optimized pack/unpack for all native format specifiers */ +/****************************************************************************/ + +/* + Fix exceptions: + 1) Include format string in the error message. + 2) OverflowError -> ValueError. + 3) The error message from PyNumber_Index() is not ideal. +*/ +static int +type_error_int(const char *fmt) +{ + PyErr_Format(PyExc_TypeError, + "memoryview: invalid type for format '%s'", fmt); + return -1; +} + +static int +value_error_int(const char *fmt) +{ + PyErr_Format(PyExc_ValueError, + "memoryview: invalid value for format '%s'", fmt); + return -1; +} + +static int +fix_error_int(const char *fmt) +{ + assert(PyErr_Occurred()); + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Clear(); + return type_error_int(fmt); + } + else if (PyErr_ExceptionMatches(PyExc_OverflowError) || + PyErr_ExceptionMatches(PyExc_ValueError)) { + PyErr_Clear(); + return value_error_int(fmt); + } + + return -1; +} + +/* Accept integer objects or objects with an __index__() method. */ +static long +pylong_as_ld(PyObject *item) +{ + PyObject *tmp; + long ld; + + tmp = PyNumber_Index(item); + if (tmp == NULL) + return -1; + + ld = PyLong_AsLong(tmp); + Py_DECREF(tmp); + return ld; +} + +static unsigned long +pylong_as_lu(PyObject *item) +{ + PyObject *tmp; + unsigned long lu; + + tmp = PyNumber_Index(item); + if (tmp == NULL) + return (unsigned long)-1; + + lu = PyLong_AsUnsignedLong(tmp); + Py_DECREF(tmp); + return lu; +} + +#ifdef HAVE_LONG_LONG +static PY_LONG_LONG +pylong_as_lld(PyObject *item) +{ + PyObject *tmp; + PY_LONG_LONG lld; + + tmp = PyNumber_Index(item); + if (tmp == NULL) + return -1; + + lld = PyLong_AsLongLong(tmp); + Py_DECREF(tmp); + return lld; +} + +static unsigned PY_LONG_LONG +pylong_as_llu(PyObject *item) +{ + PyObject *tmp; + unsigned PY_LONG_LONG llu; + + tmp = PyNumber_Index(item); + if (tmp == NULL) + return (unsigned PY_LONG_LONG)-1; + + llu = PyLong_AsUnsignedLongLong(tmp); + Py_DECREF(tmp); + return llu; +} +#endif + +static Py_ssize_t +pylong_as_zd(PyObject *item) +{ + PyObject *tmp; + Py_ssize_t zd; + + tmp = PyNumber_Index(item); + if (tmp == NULL) + return -1; + + zd = PyLong_AsSsize_t(tmp); + Py_DECREF(tmp); + return zd; +} + +static size_t +pylong_as_zu(PyObject *item) +{ + PyObject *tmp; + size_t zu; + + tmp = PyNumber_Index(item); + if (tmp == NULL) + return (size_t)-1; + + zu = PyLong_AsSize_t(tmp); + Py_DECREF(tmp); + return zu; +} + +/* Timings with the ndarray from _testbuffer.c indicate that using the + struct module is around 15x slower than the two functions below. */ + +#define UNPACK_SINGLE(dest, ptr, type) \ + do { \ + type x; \ + memcpy((char *)&x, ptr, sizeof x); \ + dest = x; \ + } while (0) + +/* Unpack a single item. 'fmt' can be any native format character in struct + module syntax. This function is very sensitive to small changes. With this + layout gcc automatically generates a fast jump table. */ +Py_LOCAL_INLINE(PyObject *) +unpack_single(const char *ptr, const char *fmt) +{ + unsigned PY_LONG_LONG llu; + unsigned long lu; + size_t zu; + PY_LONG_LONG lld; + long ld; + Py_ssize_t zd; + double d; + unsigned char uc; + void *p; + + switch (fmt[0]) { + + /* signed integers and fast path for 'B' */ + case 'B': uc = *((unsigned char *)ptr); goto convert_uc; + case 'b': ld = *((signed char *)ptr); goto convert_ld; + case 'h': UNPACK_SINGLE(ld, ptr, short); goto convert_ld; + case 'i': UNPACK_SINGLE(ld, ptr, int); goto convert_ld; + case 'l': UNPACK_SINGLE(ld, ptr, long); goto convert_ld; + + /* boolean */ + #ifdef HAVE_C99_BOOL + case '?': UNPACK_SINGLE(ld, ptr, _Bool); goto convert_bool; + #else + case '?': UNPACK_SINGLE(ld, ptr, char); goto convert_bool; + #endif + + /* unsigned integers */ + case 'H': UNPACK_SINGLE(lu, ptr, unsigned short); goto convert_lu; + case 'I': UNPACK_SINGLE(lu, ptr, unsigned int); goto convert_lu; + case 'L': UNPACK_SINGLE(lu, ptr, unsigned long); goto convert_lu; + + /* native 64-bit */ + #ifdef HAVE_LONG_LONG + case 'q': UNPACK_SINGLE(lld, ptr, PY_LONG_LONG); goto convert_lld; + case 'Q': UNPACK_SINGLE(llu, ptr, unsigned PY_LONG_LONG); goto convert_llu; + #endif + + /* ssize_t and size_t */ + case 'n': UNPACK_SINGLE(zd, ptr, Py_ssize_t); goto convert_zd; + case 'N': UNPACK_SINGLE(zu, ptr, size_t); goto convert_zu; + + /* floats */ + case 'f': UNPACK_SINGLE(d, ptr, float); goto convert_double; + case 'd': UNPACK_SINGLE(d, ptr, double); goto convert_double; + + /* bytes object */ + case 'c': goto convert_bytes; + + /* pointer */ + case 'P': UNPACK_SINGLE(p, ptr, void *); goto convert_pointer; + + /* default */ + default: goto err_format; + } + +convert_uc: + /* PyLong_FromUnsignedLong() is slower */ + return PyLong_FromLong(uc); +convert_ld: + return PyLong_FromLong(ld); +convert_lu: + return PyLong_FromUnsignedLong(lu); +convert_lld: + return PyLong_FromLongLong(lld); +convert_llu: + return PyLong_FromUnsignedLongLong(llu); +convert_zd: + return PyLong_FromSsize_t(zd); +convert_zu: + return PyLong_FromSize_t(zu); +convert_double: + return PyFloat_FromDouble(d); +convert_bool: + return PyBool_FromLong(ld); +convert_bytes: + return PyBytes_FromStringAndSize(ptr, 1); +convert_pointer: + return PyLong_FromVoidPtr(p); +err_format: + PyErr_Format(PyExc_NotImplementedError, + "memoryview: format %s not supported", fmt); + return NULL; +} + +#define PACK_SINGLE(ptr, src, type) \ + do { \ + type x; \ + x = (type)src; \ + memcpy(ptr, (char *)&x, sizeof x); \ + } while (0) + +/* Pack a single item. 'fmt' can be any native format character in + struct module syntax. */ +static int +pack_single(char *ptr, PyObject *item, const char *fmt) +{ + unsigned PY_LONG_LONG llu; + unsigned long lu; + size_t zu; + PY_LONG_LONG lld; + long ld; + Py_ssize_t zd; + double d; + void *p; + + switch (fmt[0]) { + /* signed integers */ + case 'b': case 'h': case 'i': case 'l': + ld = pylong_as_ld(item); + if (ld == -1 && PyErr_Occurred()) + goto err_occurred; + switch (fmt[0]) { + case 'b': + if (ld < SCHAR_MIN || ld > SCHAR_MAX) goto err_range; + *((signed char *)ptr) = (signed char)ld; break; + case 'h': + if (ld < SHRT_MIN || ld > SHRT_MAX) goto err_range; + PACK_SINGLE(ptr, ld, short); break; + case 'i': + if (ld < INT_MIN || ld > INT_MAX) goto err_range; + PACK_SINGLE(ptr, ld, int); break; + default: /* 'l' */ + PACK_SINGLE(ptr, ld, long); break; + } + break; + + /* unsigned integers */ + case 'B': case 'H': case 'I': case 'L': + lu = pylong_as_lu(item); + if (lu == (unsigned long)-1 && PyErr_Occurred()) + goto err_occurred; + switch (fmt[0]) { + case 'B': + if (lu > UCHAR_MAX) goto err_range; + *((unsigned char *)ptr) = (unsigned char)lu; break; + case 'H': + if (lu > USHRT_MAX) goto err_range; + PACK_SINGLE(ptr, lu, unsigned short); break; + case 'I': + if (lu > UINT_MAX) goto err_range; + PACK_SINGLE(ptr, lu, unsigned int); break; + default: /* 'L' */ + PACK_SINGLE(ptr, lu, unsigned long); break; + } + break; + + /* native 64-bit */ + #ifdef HAVE_LONG_LONG + case 'q': + lld = pylong_as_lld(item); + if (lld == -1 && PyErr_Occurred()) + goto err_occurred; + PACK_SINGLE(ptr, lld, PY_LONG_LONG); + break; + case 'Q': + llu = pylong_as_llu(item); + if (llu == (unsigned PY_LONG_LONG)-1 && PyErr_Occurred()) + goto err_occurred; + PACK_SINGLE(ptr, llu, unsigned PY_LONG_LONG); + break; + #endif + + /* ssize_t and size_t */ + case 'n': + zd = pylong_as_zd(item); + if (zd == -1 && PyErr_Occurred()) + goto err_occurred; + PACK_SINGLE(ptr, zd, Py_ssize_t); + break; + case 'N': + zu = pylong_as_zu(item); + if (zu == (size_t)-1 && PyErr_Occurred()) + goto err_occurred; + PACK_SINGLE(ptr, zu, size_t); + break; + + /* floats */ + case 'f': case 'd': + d = PyFloat_AsDouble(item); + if (d == -1.0 && PyErr_Occurred()) + goto err_occurred; + if (fmt[0] == 'f') { + PACK_SINGLE(ptr, d, float); + } + else { + PACK_SINGLE(ptr, d, double); + } + break; + + /* bool */ + case '?': + ld = PyObject_IsTrue(item); + if (ld < 0) + return -1; /* preserve original error */ + #ifdef HAVE_C99_BOOL + PACK_SINGLE(ptr, ld, _Bool); + #else + PACK_SINGLE(ptr, ld, char); + #endif + break; + + /* bytes object */ + case 'c': + if (!PyBytes_Check(item)) + return type_error_int(fmt); + if (PyBytes_GET_SIZE(item) != 1) + return value_error_int(fmt); + *ptr = PyBytes_AS_STRING(item)[0]; + break; + + /* pointer */ + case 'P': + p = PyLong_AsVoidPtr(item); + if (p == NULL && PyErr_Occurred()) + goto err_occurred; + PACK_SINGLE(ptr, p, void *); + break; + + /* default */ + default: goto err_format; + } + + return 0; + +err_occurred: + return fix_error_int(fmt); +err_range: + return value_error_int(fmt); +err_format: + PyErr_Format(PyExc_NotImplementedError, + "memoryview: format %s not supported", fmt); + return -1; +} + + +/****************************************************************************/ +/* Representations */ +/****************************************************************************/ + +/* allow explicit form of native format */ +Py_LOCAL_INLINE(const char *) +adjust_fmt(const Py_buffer *view) +{ + const char *fmt; + + fmt = (view->format[0] == '@') ? view->format+1 : view->format; + if (fmt[0] && fmt[1] == '\0') + return fmt; + + PyErr_Format(PyExc_NotImplementedError, + "memoryview: unsupported format %s", view->format); + return NULL; +} + +/* Base case for multi-dimensional unpacking. Assumption: ndim == 1. */ +static PyObject * +tolist_base(const char *ptr, const Py_ssize_t *shape, + const Py_ssize_t *strides, const Py_ssize_t *suboffsets, + const char *fmt) +{ + PyObject *lst, *item; + Py_ssize_t i; + + lst = PyList_New(shape[0]); + if (lst == NULL) + return NULL; + + for (i = 0; i < shape[0]; ptr+=strides[0], i++) { + const char *xptr = ADJUST_PTR(ptr, suboffsets); + item = unpack_single(xptr, fmt); + if (item == NULL) { + Py_DECREF(lst); + return NULL; + } + PyList_SET_ITEM(lst, i, item); + } + + return lst; +} + +/* Unpack a multi-dimensional array into a nested list. + Assumption: ndim >= 1. */ +static PyObject * +tolist_rec(const char *ptr, Py_ssize_t ndim, const Py_ssize_t *shape, + const Py_ssize_t *strides, const Py_ssize_t *suboffsets, + const char *fmt) +{ + PyObject *lst, *item; + Py_ssize_t i; + + assert(ndim >= 1); + assert(shape != NULL); + assert(strides != NULL); + + if (ndim == 1) + return tolist_base(ptr, shape, strides, suboffsets, fmt); + + lst = PyList_New(shape[0]); + if (lst == NULL) + return NULL; + + for (i = 0; i < shape[0]; ptr+=strides[0], i++) { + const char *xptr = ADJUST_PTR(ptr, suboffsets); + item = tolist_rec(xptr, ndim-1, shape+1, + strides+1, suboffsets ? suboffsets+1 : NULL, + fmt); + if (item == NULL) { + Py_DECREF(lst); + return NULL; + } + PyList_SET_ITEM(lst, i, item); + } + + return lst; +} + +/* Return a list representation of the memoryview. Currently only buffers + with native format strings are supported. */ +static PyObject * +memory_tolist(PyMemoryViewObject *mv, PyObject *noargs) +{ + const Py_buffer *view = &(mv->view); + const char *fmt; + + CHECK_RELEASED(mv); + + fmt = adjust_fmt(view); + if (fmt == NULL) + return NULL; + if (view->ndim == 0) { + return unpack_single(view->buf, fmt); + } + else if (view->ndim == 1) { + return tolist_base(view->buf, view->shape, + view->strides, view->suboffsets, + fmt); + } + else { + return tolist_rec(view->buf, view->ndim, view->shape, + view->strides, view->suboffsets, + fmt); + } +} + +static PyObject * +memory_tobytes(PyMemoryViewObject *self, PyObject *dummy) +{ + Py_buffer *src = VIEW_ADDR(self); + PyObject *bytes = NULL; + + CHECK_RELEASED(self); + + if (MV_C_CONTIGUOUS(self->flags)) { + return PyBytes_FromStringAndSize(src->buf, src->len); + } + + bytes = PyBytes_FromStringAndSize(NULL, src->len); + if (bytes == NULL) + return NULL; + + if (buffer_to_c_contiguous(PyBytes_AS_STRING(bytes), src) < 0) { + Py_DECREF(bytes); + return NULL; + } + + return bytes; +} + +static PyObject * +memory_repr(PyMemoryViewObject *self) +{ + if (self->flags & _Py_MEMORYVIEW_RELEASED) + return PyUnicode_FromFormat("", self); + else + return PyUnicode_FromFormat("", self); +} + + +/**************************************************************************/ +/* Indexing and slicing */ +/**************************************************************************/ + +/* Get the pointer to the item at index. */ +static char * +ptr_from_index(Py_buffer *view, Py_ssize_t index) +{ + char *ptr; + Py_ssize_t nitems; /* items in the first dimension */ + + assert(view->shape); + assert(view->strides); + + nitems = view->shape[0]; + if (index < 0) { + index += nitems; + } + if (index < 0 || index >= nitems) { + PyErr_SetString(PyExc_IndexError, "index out of bounds"); + return NULL; + } + + ptr = (char *)view->buf; + ptr += view->strides[0] * index; + + ptr = ADJUST_PTR(ptr, view->suboffsets); + + return ptr; +} + +/* Return the item at index. In a one-dimensional view, this is an object + with the type specified by view->format. Otherwise, the item is a sub-view. + The function is used in memory_subscript() and memory_as_sequence. */ +static PyObject * +memory_item(PyMemoryViewObject *self, Py_ssize_t index) +{ + Py_buffer *view = &(self->view); + const char *fmt; + + CHECK_RELEASED(self); + + fmt = adjust_fmt(view); + if (fmt == NULL) + return NULL; + + if (view->ndim == 0) { + PyErr_SetString(PyExc_TypeError, "invalid indexing of 0-dim memory"); + return NULL; + } + if (view->ndim == 1) { + char *ptr = ptr_from_index(view, index); + if (ptr == NULL) + return NULL; + return unpack_single(ptr, fmt); + } + + PyErr_SetString(PyExc_NotImplementedError, + "multi-dimensional sub-views are not implemented"); + return NULL; +} + +Py_LOCAL_INLINE(int) +init_slice(Py_buffer *base, PyObject *key, int dim) +{ + Py_ssize_t start, stop, step, slicelength; + + if (PySlice_GetIndicesEx(key, base->shape[dim], + &start, &stop, &step, &slicelength) < 0) { + return -1; + } + + + if (base->suboffsets == NULL || dim == 0) { + adjust_buf: + base->buf = (char *)base->buf + base->strides[dim] * start; + } + else { + Py_ssize_t n = dim-1; + while (n >= 0 && base->suboffsets[n] < 0) + n--; + if (n < 0) + goto adjust_buf; /* all suboffsets are negative */ + base->suboffsets[n] = base->suboffsets[n] + base->strides[dim] * start; + } + base->shape[dim] = slicelength; + base->strides[dim] = base->strides[dim] * step; + + return 0; +} + +static int +is_multislice(PyObject *key) +{ + Py_ssize_t size, i; + + if (!PyTuple_Check(key)) + return 0; + size = PyTuple_GET_SIZE(key); + if (size == 0) + return 0; + + for (i = 0; i < size; i++) { + PyObject *x = PyTuple_GET_ITEM(key, i); + if (!PySlice_Check(x)) + return 0; + } + return 1; +} + +/* mv[obj] returns an object holding the data for one element if obj + fully indexes the memoryview or another memoryview object if it + does not. + + 0-d memoryview objects can be referenced using mv[...] or mv[()] + but not with anything else. */ +static PyObject * +memory_subscript(PyMemoryViewObject *self, PyObject *key) +{ + Py_buffer *view; + view = &(self->view); + + CHECK_RELEASED(self); + + if (view->ndim == 0) { + if (PyTuple_Check(key) && PyTuple_GET_SIZE(key) == 0) { + const char *fmt = adjust_fmt(view); + if (fmt == NULL) + return NULL; + return unpack_single(view->buf, fmt); + } + else if (key == Py_Ellipsis) { + Py_INCREF(self); + return (PyObject *)self; + } + else { + PyErr_SetString(PyExc_TypeError, + "invalid indexing of 0-dim memory"); + return NULL; + } + } + + if (PyIndex_Check(key)) { + Py_ssize_t index; + index = PyNumber_AsSsize_t(key, PyExc_IndexError); + if (index == -1 && PyErr_Occurred()) + return NULL; + return memory_item(self, index); + } + else if (PySlice_Check(key)) { + PyMemoryViewObject *sliced; + + sliced = (PyMemoryViewObject *)mbuf_add_view(self->mbuf, view); + if (sliced == NULL) + return NULL; + + if (init_slice(&sliced->view, key, 0) < 0) { + Py_DECREF(sliced); + return NULL; + } + init_len(&sliced->view); + init_flags(sliced); + + return (PyObject *)sliced; + } + else if (is_multislice(key)) { + PyErr_SetString(PyExc_NotImplementedError, + "multi-dimensional slicing is not implemented"); + return NULL; + } + + PyErr_SetString(PyExc_TypeError, "memoryview: invalid slice key"); + return NULL; +} + +static int +memory_ass_sub(PyMemoryViewObject *self, PyObject *key, PyObject *value) +{ + Py_buffer *view = &(self->view); + Py_buffer src; + const char *fmt; + char *ptr; + + CHECK_RELEASED_INT(self); + + fmt = adjust_fmt(view); + if (fmt == NULL) + return -1; + + if (view->readonly) { + PyErr_SetString(PyExc_TypeError, "cannot modify read-only memory"); + return -1; + } + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, "cannot delete memory"); + return -1; + } + if (view->ndim == 0) { + if (key == Py_Ellipsis || + (PyTuple_Check(key) && PyTuple_GET_SIZE(key)==0)) { + ptr = (char *)view->buf; + return pack_single(ptr, value, fmt); + } + else { + PyErr_SetString(PyExc_TypeError, + "invalid indexing of 0-dim memory"); + return -1; + } + } + if (view->ndim != 1) { + PyErr_SetString(PyExc_NotImplementedError, + "memoryview assignments are currently restricted to ndim = 1"); + return -1; + } + + if (PyIndex_Check(key)) { + Py_ssize_t index = PyNumber_AsSsize_t(key, PyExc_IndexError); + if (index == -1 && PyErr_Occurred()) + return -1; + ptr = ptr_from_index(view, index); + if (ptr == NULL) + return -1; + return pack_single(ptr, value, fmt); + } + /* one-dimensional: fast path */ + if (PySlice_Check(key) && view->ndim == 1) { + Py_buffer dest; /* sliced view */ + Py_ssize_t arrays[3]; + int ret = -1; + + /* rvalue must be an exporter */ + if (PyObject_GetBuffer(value, &src, PyBUF_FULL_RO) < 0) + return ret; + + dest = *view; + dest.shape = &arrays[0]; dest.shape[0] = view->shape[0]; + dest.strides = &arrays[1]; dest.strides[0] = view->strides[0]; + if (view->suboffsets) { + dest.suboffsets = &arrays[2]; dest.suboffsets[0] = view->suboffsets[0]; + } + + if (init_slice(&dest, key, 0) < 0) + goto end_block; + dest.len = dest.shape[0] * dest.itemsize; + + ret = copy_single(&dest, &src); + + end_block: + PyBuffer_Release(&src); + return ret; + } + else if (PySlice_Check(key) || is_multislice(key)) { + /* Call memory_subscript() to produce a sliced lvalue, then copy + rvalue into lvalue. This is already implemented in _testbuffer.c. */ + PyErr_SetString(PyExc_NotImplementedError, + "memoryview slice assignments are currently restricted " + "to ndim = 1"); + return -1; + } + + PyErr_SetString(PyExc_TypeError, "memoryview: invalid slice key"); + return -1; +} + +static Py_ssize_t +memory_length(PyMemoryViewObject *self) +{ + CHECK_RELEASED_INT(self); + return self->view.ndim == 0 ? 1 : self->view.shape[0]; +} + +/* As mapping */ +static PyMappingMethods memory_as_mapping = { + (lenfunc)memory_length, /* mp_length */ + (binaryfunc)memory_subscript, /* mp_subscript */ + (objobjargproc)memory_ass_sub, /* mp_ass_subscript */ +}; + +/* As sequence */ +static PySequenceMethods memory_as_sequence = { + 0, /* sq_length */ + 0, /* sq_concat */ + 0, /* sq_repeat */ + (ssizeargfunc)memory_item, /* sq_item */ +}; + + +/**************************************************************************/ +/* Comparisons */ +/**************************************************************************/ + +#define CMP_SINGLE(p, q, type) \ + do { \ + type x; \ + type y; \ + memcpy((char *)&x, p, sizeof x); \ + memcpy((char *)&y, q, sizeof y); \ + equal = (x == y); \ + } while (0) + +Py_LOCAL_INLINE(int) +unpack_cmp(const char *p, const char *q, const char *fmt) +{ + int equal; + + switch (fmt[0]) { + + /* signed integers and fast path for 'B' */ + case 'B': return *((unsigned char *)p) == *((unsigned char *)q); + case 'b': return *((signed char *)p) == *((signed char *)q); + case 'h': CMP_SINGLE(p, q, short); return equal; + case 'i': CMP_SINGLE(p, q, int); return equal; + case 'l': CMP_SINGLE(p, q, long); return equal; + + /* boolean */ + #ifdef HAVE_C99_BOOL + case '?': CMP_SINGLE(p, q, _Bool); return equal; + #else + case '?': CMP_SINGLE(p, q, char); return equal; + #endif + + /* unsigned integers */ + case 'H': CMP_SINGLE(p, q, unsigned short); return equal; + case 'I': CMP_SINGLE(p, q, unsigned int); return equal; + case 'L': CMP_SINGLE(p, q, unsigned long); return equal; + + /* native 64-bit */ + #ifdef HAVE_LONG_LONG + case 'q': CMP_SINGLE(p, q, PY_LONG_LONG); return equal; + case 'Q': CMP_SINGLE(p, q, unsigned PY_LONG_LONG); return equal; + #endif + + /* ssize_t and size_t */ + case 'n': CMP_SINGLE(p, q, Py_ssize_t); return equal; + case 'N': CMP_SINGLE(p, q, size_t); return equal; + + /* floats */ + /* XXX DBL_EPSILON? */ + case 'f': CMP_SINGLE(p, q, float); return equal; + case 'd': CMP_SINGLE(p, q, double); return equal; + + /* bytes object */ + case 'c': return *p == *q; + + /* pointer */ + case 'P': CMP_SINGLE(p, q, void *); return equal; + + /* Py_NotImplemented */ + default: return -1; + } +} + +/* Base case for recursive array comparisons. Assumption: ndim == 1. */ +static int +cmp_base(const char *p, const char *q, const Py_ssize_t *shape, + const Py_ssize_t *pstrides, const Py_ssize_t *psuboffsets, + const Py_ssize_t *qstrides, const Py_ssize_t *qsuboffsets, + const char *fmt) +{ + Py_ssize_t i; + int equal; + + for (i = 0; i < shape[0]; p+=pstrides[0], q+=qstrides[0], i++) { + const char *xp = ADJUST_PTR(p, psuboffsets); + const char *xq = ADJUST_PTR(q, qsuboffsets); + equal = unpack_cmp(xp, xq, fmt); + if (equal <= 0) + return equal; + } + + return 1; +} + +/* Recursively compare two multi-dimensional arrays that have the same + logical structure. Assumption: ndim >= 1. */ +static int +cmp_rec(const char *p, const char *q, + Py_ssize_t ndim, const Py_ssize_t *shape, + const Py_ssize_t *pstrides, const Py_ssize_t *psuboffsets, + const Py_ssize_t *qstrides, const Py_ssize_t *qsuboffsets, + const char *fmt) +{ + Py_ssize_t i; + int equal; + + assert(ndim >= 1); + assert(shape != NULL); + assert(pstrides != NULL); + assert(qstrides != NULL); + + if (ndim == 1) { + return cmp_base(p, q, shape, + pstrides, psuboffsets, + qstrides, qsuboffsets, + fmt); + } + + for (i = 0; i < shape[0]; p+=pstrides[0], q+=qstrides[0], i++) { + const char *xp = ADJUST_PTR(p, psuboffsets); + const char *xq = ADJUST_PTR(q, qsuboffsets); + equal = cmp_rec(xp, xq, ndim-1, shape+1, + pstrides+1, psuboffsets ? psuboffsets+1 : NULL, + qstrides+1, qsuboffsets ? qsuboffsets+1 : NULL, + fmt); + if (equal <= 0) + return equal; + } + + return 1; +} + +static PyObject * +memory_richcompare(PyObject *v, PyObject *w, int op) +{ + PyObject *res; + Py_buffer wbuf, *vv, *ww = NULL; + const char *vfmt, *wfmt; + int equal = -1; /* Py_NotImplemented */ + + if (op != Py_EQ && op != Py_NE) + goto result; /* Py_NotImplemented */ + + assert(PyMemoryView_Check(v)); + if (BASE_INACCESSIBLE(v)) { + equal = (v == w); + goto result; + } + vv = VIEW_ADDR(v); + + if (PyMemoryView_Check(w)) { + if (BASE_INACCESSIBLE(w)) { + equal = (v == w); + goto result; + } + ww = VIEW_ADDR(w); + } + else { + if (PyObject_GetBuffer(w, &wbuf, PyBUF_FULL_RO) < 0) { + PyErr_Clear(); + goto result; /* Py_NotImplemented */ + } + ww = &wbuf; + } + + vfmt = adjust_fmt(vv); + wfmt = adjust_fmt(ww); + if (vfmt == NULL || wfmt == NULL) { + PyErr_Clear(); + goto result; /* Py_NotImplemented */ + } + + if (cmp_structure(vv, ww) < 0) { + PyErr_Clear(); + equal = 0; + goto result; + } + + if (vv->ndim == 0) { + equal = unpack_cmp(vv->buf, ww->buf, vfmt); + } + else if (vv->ndim == 1) { + equal = cmp_base(vv->buf, ww->buf, vv->shape, + vv->strides, vv->suboffsets, + ww->strides, ww->suboffsets, + vfmt); + } + else { + equal = cmp_rec(vv->buf, ww->buf, vv->ndim, vv->shape, + vv->strides, vv->suboffsets, + ww->strides, ww->suboffsets, + vfmt); + } + +result: + if (equal < 0) + res = Py_NotImplemented; + else if ((equal && op == Py_EQ) || (!equal && op == Py_NE)) + res = Py_True; + else + res = Py_False; + + if (ww == &wbuf) + PyBuffer_Release(ww); + Py_INCREF(res); + return res; +} + +/**************************************************************************/ +/* Hash */ +/**************************************************************************/ + +static Py_hash_t +memory_hash(PyMemoryViewObject *self) +{ + if (self->hash == -1) { + Py_buffer *view = &self->view; + char *mem = view->buf; + + CHECK_RELEASED_INT(self); + + if (!view->readonly) { + PyErr_SetString(PyExc_ValueError, + "cannot hash writable memoryview object"); + return -1; + } + if (view->obj != NULL && PyObject_Hash(view->obj) == -1) { + /* Keep the original error message */ + return -1; + } + + if (!MV_C_CONTIGUOUS(self->flags)) { + mem = PyMem_Malloc(view->len); + if (mem == NULL) { + PyErr_NoMemory(); + return -1; + } + if (buffer_to_c_contiguous(mem, view) < 0) { + PyMem_Free(mem); + return -1; + } + } + + /* Can't fail */ + self->hash = _Py_HashBytes((unsigned char *)mem, view->len); + + if (mem != view->buf) + PyMem_Free(mem); + } + + return self->hash; +} + + +/**************************************************************************/ +/* getters */ +/**************************************************************************/ + +static PyObject * +_IntTupleFromSsizet(int len, Py_ssize_t *vals) +{ + int i; + PyObject *o; + PyObject *intTuple; + + if (vals == NULL) + return PyTuple_New(0); + + intTuple = PyTuple_New(len); + if (!intTuple) + return NULL; + for (i=0; iview; + + CHECK_RELEASED(self); + if (view->obj == NULL) { + Py_RETURN_NONE; + } + Py_INCREF(view->obj); + return view->obj; +} + +static PyObject * +memory_nbytes_get(PyMemoryViewObject *self) +{ + CHECK_RELEASED(self); + return PyLong_FromSsize_t(self->view.len); +} + static PyObject * memory_format_get(PyMemoryViewObject *self) { @@ -343,31 +2502,6 @@ memory_itemsize_get(PyMemoryViewObject *self) return PyLong_FromSsize_t(self->view.itemsize); } -static PyObject * -_IntTupleFromSsizet(int len, Py_ssize_t *vals) -{ - int i; - PyObject *o; - PyObject *intTuple; - - if (vals == NULL) { - Py_INCREF(Py_None); - return Py_None; - } - intTuple = PyTuple_New(len); - if (!intTuple) - return NULL; - for (i=0; iview.ndim); } -static PyGetSetDef memory_getsetlist[] ={ - {"format", (getter)memory_format_get, NULL, NULL}, - {"itemsize", (getter)memory_itemsize_get, NULL, NULL}, - {"shape", (getter)memory_shape_get, NULL, NULL}, - {"strides", (getter)memory_strides_get, NULL, NULL}, - {"suboffsets", (getter)memory_suboffsets_get, NULL, NULL}, - {"readonly", (getter)memory_readonly_get, NULL, NULL}, - {"ndim", (getter)memory_ndim_get, NULL, NULL}, +static PyObject * +memory_c_contiguous(PyMemoryViewObject *self, PyObject *dummy) +{ + CHECK_RELEASED(self); + return PyBool_FromLong(MV_C_CONTIGUOUS(self->flags)); +} + +static PyObject * +memory_f_contiguous(PyMemoryViewObject *self, PyObject *dummy) +{ + CHECK_RELEASED(self); + return PyBool_FromLong(MV_F_CONTIGUOUS(self->flags)); +} + +static PyObject * +memory_contiguous(PyMemoryViewObject *self, PyObject *dummy) +{ + CHECK_RELEASED(self); + return PyBool_FromLong(MV_ANY_CONTIGUOUS(self->flags)); +} + +static PyGetSetDef memory_getsetlist[] = { + {"obj", (getter)memory_obj_get, NULL, NULL}, + {"nbytes", (getter)memory_nbytes_get, NULL, NULL}, + {"readonly", (getter)memory_readonly_get, NULL, NULL}, + {"itemsize", (getter)memory_itemsize_get, NULL, NULL}, + {"format", (getter)memory_format_get, NULL, NULL}, + {"ndim", (getter)memory_ndim_get, NULL, NULL}, + {"shape", (getter)memory_shape_get, NULL, NULL}, + {"strides", (getter)memory_strides_get, NULL, NULL}, + {"suboffsets", (getter)memory_suboffsets_get, NULL, NULL}, + {"c_contiguous", (getter)memory_c_contiguous, NULL, NULL}, + {"f_contiguous", (getter)memory_f_contiguous, NULL, NULL}, + {"contiguous", (getter)memory_contiguous, NULL, NULL}, {NULL, NULL, NULL, NULL}, }; -static PyObject * -memory_tobytes(PyMemoryViewObject *mem, PyObject *noargs) -{ - CHECK_RELEASED(mem); - return PyObject_CallFunctionObjArgs( - (PyObject *) &PyBytes_Type, mem, NULL); -} - -/* TODO: rewrite this function using the struct module to unpack - each buffer item */ - -static PyObject * -memory_tolist(PyMemoryViewObject *mem, PyObject *noargs) -{ - Py_buffer *view = &(mem->view); - Py_ssize_t i; - PyObject *res, *item; - char *buf; - - CHECK_RELEASED(mem); - if (strcmp(view->format, "B") || view->itemsize != 1) { - PyErr_SetString(PyExc_NotImplementedError, - "tolist() only supports byte views"); - return NULL; - } - if (view->ndim != 1) { - PyErr_SetString(PyExc_NotImplementedError, - "tolist() only supports one-dimensional objects"); - return NULL; - } - res = PyList_New(view->len); - if (res == NULL) - return NULL; - buf = view->buf; - for (i = 0; i < view->len; i++) { - item = PyLong_FromUnsignedLong((unsigned char) *buf); - if (item == NULL) { - Py_DECREF(res); - return NULL; - } - PyList_SET_ITEM(res, i, item); - buf++; - } - return res; -} - -static void -do_release(PyMemoryViewObject *self) -{ - if (self->view.obj != NULL) { - PyBuffer_Release(&(self->view)); - } - self->view.obj = NULL; - self->view.buf = NULL; -} - -static PyObject * -memory_enter(PyObject *self, PyObject *args) -{ - CHECK_RELEASED(self); - Py_INCREF(self); - return self; -} - -static PyObject * -memory_exit(PyObject *self, PyObject *args) -{ - do_release((PyMemoryViewObject *) self); - Py_RETURN_NONE; -} - static PyMethodDef memory_methods[] = { - {"release", memory_exit, METH_NOARGS}, - {"tobytes", (PyCFunction)memory_tobytes, METH_NOARGS, NULL}, - {"tolist", (PyCFunction)memory_tolist, METH_NOARGS, NULL}, - {"__enter__", memory_enter, METH_NOARGS}, - {"__exit__", memory_exit, METH_VARARGS}, - {NULL, NULL} /* sentinel */ -}; - - -static void -memory_dealloc(PyMemoryViewObject *self) -{ - _PyObject_GC_UNTRACK(self); - do_release(self); - PyObject_GC_Del(self); -} - -static PyObject * -memory_repr(PyMemoryViewObject *self) -{ - if (IS_RELEASED(self)) - return PyUnicode_FromFormat("", self); - else - return PyUnicode_FromFormat("", self); -} - -static Py_hash_t -memory_hash(PyMemoryViewObject *self) -{ - if (self->hash == -1) { - Py_buffer *view = &self->view; - CHECK_RELEASED_INT(self); - if (view->ndim > 1) { - PyErr_SetString(PyExc_NotImplementedError, - "can't hash multi-dimensional memoryview object"); - return -1; - } - if (view->strides && view->strides[0] != view->itemsize) { - PyErr_SetString(PyExc_NotImplementedError, - "can't hash strided memoryview object"); - return -1; - } - if (!view->readonly) { - PyErr_SetString(PyExc_ValueError, - "can't hash writable memoryview object"); - return -1; - } - if (view->obj != NULL && PyObject_Hash(view->obj) == -1) { - /* Keep the original error message */ - return -1; - } - /* Can't fail */ - self->hash = _Py_HashBytes((unsigned char *) view->buf, view->len); - } - return self->hash; -} - -/* Sequence methods */ -static Py_ssize_t -memory_length(PyMemoryViewObject *self) -{ - CHECK_RELEASED_INT(self); - return get_shape0(&self->view); -} - -/* Alternate version of memory_subcript that only accepts indices. - Used by PySeqIter_New(). -*/ -static PyObject * -memory_item(PyMemoryViewObject *self, Py_ssize_t result) -{ - Py_buffer *view = &(self->view); - - CHECK_RELEASED(self); - if (view->ndim == 0) { - PyErr_SetString(PyExc_IndexError, - "invalid indexing of 0-dim memory"); - return NULL; - } - if (view->ndim == 1) { - /* Return a bytes object */ - char *ptr; - ptr = (char *)view->buf; - if (result < 0) { - result += get_shape0(view); - } - if ((result < 0) || (result >= get_shape0(view))) { - PyErr_SetString(PyExc_IndexError, - "index out of bounds"); - return NULL; - } - if (view->strides == NULL) - ptr += view->itemsize * result; - else - ptr += view->strides[0] * result; - if (view->suboffsets != NULL && - view->suboffsets[0] >= 0) { - ptr = *((char **)ptr) + view->suboffsets[0]; - } - return PyBytes_FromStringAndSize(ptr, view->itemsize); - } else { - /* Return a new memory-view object */ - Py_buffer newview; - memset(&newview, 0, sizeof(newview)); - /* XXX: This needs to be fixed so it actually returns a sub-view */ - return PyMemoryView_FromBuffer(&newview); - } -} - -/* - mem[obj] returns a bytes object holding the data for one element if - obj fully indexes the memory view or another memory-view object - if it does not. - - 0-d memory-view objects can be referenced using ... or () but - not with anything else. - */ -static PyObject * -memory_subscript(PyMemoryViewObject *self, PyObject *key) -{ - Py_buffer *view; - view = &(self->view); - - CHECK_RELEASED(self); - if (view->ndim == 0) { - if (key == Py_Ellipsis || - (PyTuple_Check(key) && PyTuple_GET_SIZE(key)==0)) { - Py_INCREF(self); - return (PyObject *)self; - } - else { - PyErr_SetString(PyExc_IndexError, - "invalid indexing of 0-dim memory"); - return NULL; - } - } - if (PyIndex_Check(key)) { - Py_ssize_t result; - result = PyNumber_AsSsize_t(key, NULL); - if (result == -1 && PyErr_Occurred()) - return NULL; - return memory_item(self, result); - } - else if (PySlice_Check(key)) { - Py_ssize_t start, stop, step, slicelength; - - if (PySlice_GetIndicesEx(key, get_shape0(view), - &start, &stop, &step, &slicelength) < 0) { - return NULL; - } - - if (step == 1 && view->ndim == 1) { - Py_buffer newview; - void *newbuf = (char *) view->buf - + start * view->itemsize; - int newflags = view->readonly - ? PyBUF_CONTIG_RO : PyBUF_CONTIG; - - /* XXX There should be an API to create a subbuffer */ - if (view->obj != NULL) { - if (PyObject_GetBuffer(view->obj, &newview, newflags) == -1) - return NULL; - } - else { - newview = *view; - } - newview.buf = newbuf; - newview.len = slicelength * newview.itemsize; - newview.format = view->format; - newview.shape = &(newview.smalltable[0]); - newview.shape[0] = slicelength; - newview.strides = &(newview.itemsize); - return PyMemoryView_FromBuffer(&newview); - } - PyErr_SetNone(PyExc_NotImplementedError); - return NULL; - } - PyErr_Format(PyExc_TypeError, - "cannot index memory using \"%.200s\"", - key->ob_type->tp_name); - return NULL; -} - - -/* Need to support assigning memory if we can */ -static int -memory_ass_sub(PyMemoryViewObject *self, PyObject *key, PyObject *value) -{ - Py_ssize_t start, len, bytelen; - Py_buffer srcview; - Py_buffer *view = &(self->view); - char *srcbuf, *destbuf; - - CHECK_RELEASED_INT(self); - if (view->readonly) { - PyErr_SetString(PyExc_TypeError, - "cannot modify read-only memory"); - return -1; - } - if (value == NULL) { - PyErr_SetString(PyExc_TypeError, - "cannot delete memory"); - return -1; - } - if (view->ndim != 1) { - PyErr_SetNone(PyExc_NotImplementedError); - return -1; - } - if (PyIndex_Check(key)) { - start = PyNumber_AsSsize_t(key, NULL); - if (start == -1 && PyErr_Occurred()) - return -1; - if (start < 0) { - start += get_shape0(view); - } - if ((start < 0) || (start >= get_shape0(view))) { - PyErr_SetString(PyExc_IndexError, - "index out of bounds"); - return -1; - } - len = 1; - } - else if (PySlice_Check(key)) { - Py_ssize_t stop, step; - - if (PySlice_GetIndicesEx(key, get_shape0(view), - &start, &stop, &step, &len) < 0) { - return -1; - } - if (step != 1) { - PyErr_SetNone(PyExc_NotImplementedError); - return -1; - } - } - else { - PyErr_Format(PyExc_TypeError, - "cannot index memory using \"%.200s\"", - key->ob_type->tp_name); - return -1; - } - if (PyObject_GetBuffer(value, &srcview, PyBUF_CONTIG_RO) == -1) { - return -1; - } - /* XXX should we allow assignment of different item sizes - as long as the byte length is the same? - (e.g. assign 2 shorts to a 4-byte slice) */ - if (srcview.itemsize != view->itemsize) { - PyErr_Format(PyExc_TypeError, - "mismatching item sizes for \"%.200s\" and \"%.200s\"", - view->obj->ob_type->tp_name, srcview.obj->ob_type->tp_name); - goto _error; - } - bytelen = len * view->itemsize; - if (bytelen != srcview.len) { - PyErr_SetString(PyExc_ValueError, - "cannot modify size of memoryview object"); - goto _error; - } - /* Do the actual copy */ - destbuf = (char *) view->buf + start * view->itemsize; - srcbuf = (char *) srcview.buf; - if (destbuf + bytelen < srcbuf || srcbuf + bytelen < destbuf) - /* No overlapping */ - memcpy(destbuf, srcbuf, bytelen); - else - memmove(destbuf, srcbuf, bytelen); - - PyBuffer_Release(&srcview); - return 0; - -_error: - PyBuffer_Release(&srcview); - return -1; -} - -static PyObject * -memory_richcompare(PyObject *v, PyObject *w, int op) -{ - Py_buffer vv, ww; - int equal = 0; - PyObject *res; - - vv.obj = NULL; - ww.obj = NULL; - if (op != Py_EQ && op != Py_NE) - goto _notimpl; - if ((PyMemoryView_Check(v) && IS_RELEASED(v)) || - (PyMemoryView_Check(w) && IS_RELEASED(w))) { - equal = (v == w); - goto _end; - } - if (PyObject_GetBuffer(v, &vv, PyBUF_CONTIG_RO) == -1) { - PyErr_Clear(); - goto _notimpl; - } - if (PyObject_GetBuffer(w, &ww, PyBUF_CONTIG_RO) == -1) { - PyErr_Clear(); - goto _notimpl; - } - - if (vv.itemsize != ww.itemsize || vv.len != ww.len) - goto _end; - - equal = !memcmp(vv.buf, ww.buf, vv.len); - -_end: - PyBuffer_Release(&vv); - PyBuffer_Release(&ww); - if ((equal && op == Py_EQ) || (!equal && op == Py_NE)) - res = Py_True; - else - res = Py_False; - Py_INCREF(res); - return res; - -_notimpl: - PyBuffer_Release(&vv); - PyBuffer_Release(&ww); - Py_RETURN_NOTIMPLEMENTED; -} - - -static int -memory_traverse(PyMemoryViewObject *self, visitproc visit, void *arg) -{ - if (self->view.obj != NULL) - Py_VISIT(self->view.obj); - return 0; -} - -static int -memory_clear(PyMemoryViewObject *self) -{ - PyBuffer_Release(&self->view); - return 0; -} - - -/* As mapping */ -static PyMappingMethods memory_as_mapping = { - (lenfunc)memory_length, /* mp_length */ - (binaryfunc)memory_subscript, /* mp_subscript */ - (objobjargproc)memory_ass_sub, /* mp_ass_subscript */ -}; - -static PySequenceMethods memory_as_sequence = { - 0, /* sq_length */ - 0, /* sq_concat */ - 0, /* sq_repeat */ - (ssizeargfunc)memory_item, /* sq_item */ -}; - -/* Buffer methods */ - -static PyBufferProcs memory_as_buffer = { - (getbufferproc)memory_getbuf, /* bf_getbuffer */ - (releasebufferproc)memory_releasebuf, /* bf_releasebuffer */ + {"release", (PyCFunction)memory_release, METH_NOARGS}, + {"tobytes", (PyCFunction)memory_tobytes, METH_NOARGS, NULL}, + {"tolist", (PyCFunction)memory_tolist, METH_NOARGS, NULL}, + {"cast", (PyCFunction)memory_cast, METH_VARARGS|METH_KEYWORDS, NULL}, + {"__enter__", memory_enter, METH_NOARGS}, + {"__exit__", memory_exit, METH_VARARGS}, + {NULL, NULL} }; PyTypeObject PyMemoryView_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) - "memoryview", - sizeof(PyMemoryViewObject), - 0, + "memoryview", /* tp_name */ + offsetof(PyMemoryViewObject, ob_array), /* tp_basicsize */ + sizeof(Py_ssize_t), /* tp_itemsize */ (destructor)memory_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ diff --git a/Objects/object.c b/Objects/object.c index 2665d2135fc..70d320ca353 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1650,6 +1650,9 @@ _Py_ReadyTypes(void) if (PyType_Ready(&PyProperty_Type) < 0) Py_FatalError("Can't initialize property type"); + if (PyType_Ready(&_PyManagedBuffer_Type) < 0) + Py_FatalError("Can't initialize managed buffer type"); + if (PyType_Ready(&PyMemoryView_Type) < 0) Py_FatalError("Can't initialize memoryview type"); diff --git a/PCbuild/_testbuffer.vcproj b/PCbuild/_testbuffer.vcproj new file mode 100644 index 00000000000..795ea277690 --- /dev/null +++ b/PCbuild/_testbuffer.vcproj @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PCbuild/pcbuild.sln b/PCbuild/pcbuild.sln index 9efb6d94962..992e66a166b 100644 --- a/PCbuild/pcbuild.sln +++ b/PCbuild/pcbuild.sln @@ -142,6 +142,11 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "python3dll", "python3dll.vc EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "xxlimited", "xxlimited.vcproj", "{F749B822-B489-4CA5-A3AD-CE078F5F338A}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "_testbuffer", "_testbuffer.vcproj", "{A2697BD3-28C1-4AEC-9106-8B748639FD16}" + ProjectSection(ProjectDependencies) = postProject + {CF7AC3D1-E2DF-41D2-BEA6-1E2556CDEA26} = {CF7AC3D1-E2DF-41D2-BEA6-1E2556CDEA26} + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 @@ -609,6 +614,22 @@ Global {F749B822-B489-4CA5-A3AD-CE078F5F338A}.Release|Win32.Build.0 = Release|Win32 {F749B822-B489-4CA5-A3AD-CE078F5F338A}.Release|x64.ActiveCfg = Release|x64 {F749B822-B489-4CA5-A3AD-CE078F5F338A}.Release|x64.Build.0 = Release|x64 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.Debug|Win32.ActiveCfg = Debug|Win32 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.Debug|Win32.Build.0 = Debug|Win32 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.Debug|x64.ActiveCfg = Debug|x64 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.Debug|x64.Build.0 = Debug|x64 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.PGInstrument|Win32.ActiveCfg = PGInstrument|Win32 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.PGInstrument|Win32.Build.0 = PGInstrument|Win32 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.PGInstrument|x64.ActiveCfg = PGInstrument|x64 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.PGInstrument|x64.Build.0 = PGInstrument|x64 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.PGUpdate|Win32.ActiveCfg = PGUpdate|Win32 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.PGUpdate|Win32.Build.0 = PGUpdate|Win32 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.PGUpdate|x64.ActiveCfg = PGUpdate|x64 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.PGUpdate|x64.Build.0 = PGUpdate|x64 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.Release|Win32.ActiveCfg = Release|Win32 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.Release|Win32.Build.0 = Release|Win32 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.Release|x64.ActiveCfg = Release|x64 + {A2697BD3-28C1-4AEC-9106-8B748639FD16}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PCbuild/readme.txt b/PCbuild/readme.txt index f47d555248a..2146221b1d7 100644 --- a/PCbuild/readme.txt +++ b/PCbuild/readme.txt @@ -92,6 +92,9 @@ _socket _testcapi tests of the Python C API, run via Lib/test/test_capi.py, and implemented by module Modules/_testcapimodule.c +_testbuffer + buffer protocol tests, run via Lib/test/test_buffer.py, and + implemented by module Modules/_testbuffer.c pyexpat Python wrapper for accelerated XML parsing, which incorporates stable code from the Expat project: http://sourceforge.net/projects/expat/ diff --git a/setup.py b/setup.py index 39aff67ab35..cad293b1fac 100644 --- a/setup.py +++ b/setup.py @@ -530,6 +530,8 @@ class PyBuildExt(build_ext): # Python C API test module exts.append( Extension('_testcapi', ['_testcapimodule.c'], depends=['testcapi_long.h']) ) + # Python PEP-3118 (buffer protocol) test module + exts.append( Extension('_testbuffer', ['_testbuffer.c']) ) # profiler (_lsprof is for cProfile.py) exts.append( Extension('_lsprof', ['_lsprof.c', 'rotatingtree.c']) ) # static Unicode character database