Commits

Anonymous committed 6165aa1

Buffer API: beef up PyBufferTest.java and fix zero-length slice bug.
Work on the io library revealed a bug in slicing that affected zero-length slices. I built up the unit test where it dealt with slices. This showed up the zero-length bug, other new bugs, and some weak design that I rectified by removal of BufferPointer.size. PyBufferTest, test_bytes, test_memoryview pass.

Comments (0)

Files changed (11)

src/org/python/core/BufferPointer.java

 package org.python.core;
 
 /**
- * A class that references a contiguous slice of a <code>byte[]</code> array for use in the buffer
- * API. This class simply bundles together a refernce to an array, a starting offset within that
- * array, and specification of the number of bytes that may validly be accessed at that offset. It
- * is used by the Jython buffer API roughly where the CPython buffer API uses a C (char *) pointer,
- * or such a pointer and a length.
+ * A class that references a specified <code>byte[]</code> array and an offset in it to be treated
+ * as "index zero", for use in the buffer API. This class simply bundles together a reference to an
+ * array and a particular offset within that array. It is used by the Jython buffer API roughly
+ * where the CPython buffer API uses a C (char *) pointer.
  */
 public class BufferPointer {
 
     /**
-     * Reference to the array holding the bytes. Usually this is the actual storage exported by a
-     * Python object. In some contexts the consumer will be entitled to make changes to the contents
-     * of this array, and in others, not. See {@link PyBuffer#isReadonly()}.
+     * Reference to the backing array. Usually this is the actual storage exported by a Python
+     * object. In some contexts the consumer will be entitled to make changes to the contents of
+     * this array, and in others, not. See {@link PyBuffer#isReadonly()}.
      */
     public final byte[] storage;
-    /** Starting position within the array for the data being pointed to. */
+    /** Starting position within the array for index calculations: "index zero". */
     public final int offset;
-    /** Number of bytes within the array comprising the data being pointed to. */
-    public final int size;
 
     /**
-     * Refer to a contiguous slice of the given array.
-     * 
+     * Refer to an offset in the given array.
+     *
      * @param storage array at reference
      * @param offset index of the first byte
-     * @param size number of bytes being referred to
      */
-    public BufferPointer(byte[] storage, int offset, int size) {
-        if ((offset | size | (storage.length-(offset + size))) < 0) {
-            throw Py.BufferError("Indexing error in buffer API");
-        }
+    public BufferPointer(byte[] storage, int offset) {
+        // No checks: keep it simple
         this.storage = storage;
         this.offset = offset;
-        this.size = size;
     }
 
     /**
      * Refer to the whole of a byte array.
-     * 
+     *
      * @param storage array at reference
      */
     public BufferPointer(byte[] storage) {
         this.storage = storage;
         this.offset = 0;
-        this.size = storage.length;
     }
 }

src/org/python/core/PyBUF.java

      */
     static final int RECORDS_RO = STRIDES | FORMAT;
     /**
-     * Equivalent to <code>(INDIRECT | WRITABLE | FORMAT)</code>. Also use this as the request flags
-     * if you plan only to use the fully-encapsulated API (<code>byteAt</code>, <code>storeAt</code>
-     * , <code>copyTo</code>, <code>copyFrom</code>, etc.), without ever calling
+     * Equivalent to <code>(INDIRECT | WRITABLE | FORMAT)</code>. Also use this in the request if
+     * you plan only to use the fully-encapsulated API (<code>byteAt</code>, <code>storeAt</code>,
+     * <code>copyTo</code>, <code>copyFrom</code>, etc.), without ever calling
      * {@link PyBuffer#getBuf()}.
      */
     static final int FULL = INDIRECT | WRITABLE | FORMAT;
     /**
-     * Equivalent to <code>(INDIRECT | FORMAT)</code> Also use this as the request flags if you plan
-     * only to use the fully-encapsulated API (<code>byteAt</code>, <code>copyTo</code>, etc.),
-     * without ever calling {@link PyBuffer#getBuf()}.
+     * Equivalent to <code>(INDIRECT | FORMAT)</code>. Also use this in the request if you plan only
+     * to use the fully-encapsulated API (<code>byteAt</code>, <code>copyTo</code>, etc.), read
+     * only, without ever calling {@link PyBuffer#getBuf()}.
      */
     static final int FULL_RO = INDIRECT | FORMAT;
 

src/org/python/core/PyBuffer.java

     BufferPointer getBuf();
 
     /**
-     * Return a structure describing the slice of a byte array that holds a single item from the
+     * Return a structure describing the slice of a byte array that points to a single item from the
      * data being exported to the consumer. For a one-dimensional contiguous buffer, assuming the
      * following client code where <code>obj</code> has type <code>BufferProtocol</code>:
      *
      * <code>[b.offset]</code> to <code>[b.offset + itemsize - 1]</code> inclusive. And if
      * <code>itemsize==1</code>, the item is simply the byte <code>b.storage[b.offset]</code>
      * <p>
-     * Essentially this is a method for computing the offset of a particular index. Although
-     * <code>b.size==itemsize</code>, the client is free to navigate the underlying buffer
-     * <code>b.storage</code> without respecting these boundaries.
+     * Essentially this is a method for computing the offset of a particular index. The client is
+     * free to navigate the underlying buffer <code>b.storage</code> without respecting these
+     * boundaries.
      *
      * @param index in the buffer to position the pointer
      * @return structure defining the byte[] slice that is the shared data
     BufferPointer getPointer(int index);
 
     /**
-     * Return a structure describing the slice of a byte array that holds a single item from the
+     * Return a structure describing the slice of a byte array that points to a single item from the
      * data being exported to the consumer, in the case that array may be multi-dimensional. For a
      * 3-dimensional contiguous buffer, assuming the following client code where <code>obj</code>
      * has type <code>BufferProtocol</code>:
      * <code>[b.offset]</code> to <code>[b.offset + itemsize - 1]</code> inclusive. And if
      * <code>itemsize==1</code>, the item is simply the byte <code>b.storage[b.offset]</code>
      * <p>
-     * Essentially this is a method for computing the offset of a particular index. Although
-     * <code>b.size==itemsize</code>, the client is free to navigate the underlying buffer
-     * <code>b.storage</code> without respecting these boundaries.
+     * Essentially this is a method for computing the offset of a particular index. The client is
+     * free to navigate the underlying buffer <code>b.storage</code> without respecting these
+     * boundaries.
      * <p>
      * If the buffer is also non-contiguous, <code>b.storage[b.offset]</code> is still the (first
      * byte of) the item at index [0,...,0]. However, it is necessary to navigate <code>b</code>
     // Inherited from PyBUF and belonging here
     //
     // int getItemsize();
+
+    /**
+     * The toString() method of a buffer reproduces the byte values in the buffer (treated as
+     * unsigned integers) as the character codes of a <code>String</code>.
+     */
+    @Override
+    public String toString();
 }

src/org/python/core/buffer/BaseBuffer.java

 
 /**
  * Base implementation of the Buffer API providing variables and accessors for the navigational
- * arrays (without actually creating the arrays), methods for expressing and checking the buffer
- * request flags, methods and mechanism for get-release counting, boilerplate error checks and their
- * associated exceptions, and default implementations of some methods for access to the buffer
- * content. The design aim is to ensure unglamorous common code need only be implemented once.
+ * arrays, methods for expressing and checking the buffer request flags, methods and mechanism for
+ * get-release counting, boilerplate error checks and their associated exceptions, and default
+ * implementations of some methods for access to the buffer content. The design aim is to ensure
+ * unglamorous common code need only be implemented once.
  * <p>
  * Where provided, the buffer access methods are appropriate to 1-dimensional arrays where the units
  * are single bytes, stored contiguously. Sub-classes that deal with N-dimensional arrays,
- * discontiguous storage and items that are not single bytes must override the default
+ * non-contiguous storage and items that are not single bytes must override the default
  * implementations.
  * <p>
  * This base implementation is writable only if {@link PyBUF#WRITABLE} is in the feature flags
 
     @Override
     public boolean isReadonly() {
-        return (gFeatureFlags & WRITABLE) == 0;
+        // WRITABLE is a non-navigational flag, so is inverted in gFeatureFlags
+        return (gFeatureFlags & WRITABLE) != 0;
     }
 
     @Override
 
     @Override
     public int getLen() {
-        // Correct if one-dimensional. Override if N-dimensional with itemsize*product(shape).
+        // Correct if one-dimensional bytes. Override with itemsize*product(shape).
         return shape[0];
     }
 
             System.arraycopy(buf.storage, s, dest, d, length * itemsize);
 
         } else if (itemsize == 1) {
-            // Discontiguous copy: single byte items
+            // Non-contiguous copy: single byte items
             int limit = s + length * stride;
             for (; s < limit; s += stride) {
                 dest[d++] = buf.storage[s];
             }
 
         } else {
-            // Discontiguous copy: each time, copy itemsize bytes then skip
+            // Non-contiguous copy: each time, copy itemsize bytes then skip
             int limit = s + length * stride;
             for (; s < limit; s += skip) {
                 int t = s + itemsize;
             System.arraycopy(src, srcPos, buf.storage, d, length * itemsize);
 
         } else if (itemsize == 1) {
-            // Discontiguous copy: single byte items
+            // Non-contiguous copy: single byte items
             int limit = d + length * stride;
             for (; d != limit; d += stride) {
                 buf.storage[d] = src[s++];
             }
 
         } else {
-            // Discontiguous copy: each time, copy itemsize bytes then skip
+            // Non-contiguous copy: each time, copy itemsize bytes then skip
             int limit = d + length * stride;
             for (; d != limit; d += skip) {
                 int t = d + itemsize;
         // Block operation if read-only and same length
         if (isReadonly()) {
             throw notWritable();
-        } else if (src.getLen() != buf.size || src.getItemsize() != getItemsize()) {
+        } else if (src.getLen() != getLen() || src.getItemsize() != getItemsize()) {
             throw differentStructure();
         }
 
             src.copyTo(buf.storage, d);
 
         } else if (itemsize == 1) {
-            // Discontiguous copy: single byte items
+            // Non-contiguous copy: single byte items
             int limit = d + src.getLen() * stride;
             for (; d != limit; d += stride) {
                 buf.storage[d] = src.byteAt(s++);
             }
 
         } else {
-            // Discontiguous copy: each time, copy itemsize bytes then skip
+            // Non-contiguous copy: each time, copy itemsize bytes then skip
             int limit = d + src.getShape()[0] * stride;
             for (; d != limit; d += stride) {
                 BufferPointer srcItem = src.getPointer(s++);
      * buffer view of the exporter's state all remain valid. We do not let consumers do this through
      * the {@link PyBuffer} interface: from their perspective, calling {@link PyBuffer#release()}
      * should mean the end of their access, although we can't stop them holding a reference to the
-     * PyBuffer. Only the exporting object, which is handles the implementation type is trusted to
-     * know when re-use is safe.
+     * PyBuffer. Only the exporting object, which handles the implementation type is trusted to know
+     * when re-use is safe.
      * <p>
      * An exporter will use this method as part of its implementation of
      * {@link BufferProtocol#getBuffer(int)}. On return from that, the buffer <i>and the exporting
 
     @Override
     public BufferPointer getPointer(int index) {
-        return new BufferPointer(buf.storage, calcIndex(index), getItemsize());
+        return new BufferPointer(buf.storage, calcIndex(index));
     }
 
     @Override
     public BufferPointer getPointer(int... indices) {
-        return new BufferPointer(buf.storage, calcIndex(indices), getItemsize());
+        return new BufferPointer(buf.storage, calcIndex(indices));
     }
 
     @Override
     }
 
     /**
-     * Check that the argument is within the buffer <code>buf</code>. An exception is raised if
-     * <code>i&lt;buf.offset</code> or <code>i&gt;buf.offset+buf.size-1</code>
+     * Convenience method for checking arguments to slice formation, that the start and end elements
+     * are within the buffer. An exception is raised if <code>start&lt;0</code> or
+     * <code>start+length&gt;shape[0]</code>. This logic is correct for one-dimensional arrays (of
+     * any item size) and stride. In the context we use this, <code>length</code> is guaranteed by
+     * the acller to be non-negative.
      *
-     * @param i index to check
-     * @throws IndexOutOfBoundsException if <code>i&lt;buf.offset</code> or
-     *             <code>i&gt;buf.offset+buf.size-1</code>.
+     * @param start index to check
+     * @param length number of elements in slice (must be &gt;0)
+     * @throws IndexOutOfBoundsException
      */
-    protected void checkInBuf(int i) throws IndexOutOfBoundsException {
-        int a = buf.offset;
-        int b = a + buf.size - 1;
-        // Check: b >= i >= a. Cheat.
-        if (((i - a) | (b - i)) < 0) {
-            throw new IndexOutOfBoundsException();
+    protected void checkSlice(int start, int length) throws IndexOutOfBoundsException {
+        // Between the last element of the slice and the end of the buffer there are ...
+        int margin = shape[0] - start - length;
+        if ((start | margin) < 0) {
+            throw new IndexOutOfBoundsException("invalid slice of buffer");
         }
     }
 
     /**
-     * Check that the both arguments are within the buffer <code>buf</code>. An exception is raised
-     * if <code>i&lt;buf.offset</code>, <code>j&lt;buf.offset</code>,
-     * <code>i&gt;buf.offset+buf.size-1</code>, or <code>j&gt;buf.offset+buf.size-1</code>
+     * Convenience method for checking arguments to slice formation, that the start and end elements
+     * are within the buffer. An exception is raised if either of <code>start</code> or
+     * <code>end=start+(length-1)*stride+1</code> is <code>&lt;0</code> or <code>&gt;shape[0]</code>
+     * . This logic is correct for one-dimensional arrays (of any item size) and current stride.
+     * Note that the parameter stride is in terms of this biuffer's indexing. In the context we use
+     * this, <code>length</code> is guaranteed by the acller to be non-negative.
      *
-     * @param i index to check
-     * @param j index to check
-     * @throws IndexOutOfBoundsException if <code>i&lt;buf.offset</code> or
-     *             <code>i&gt;buf.offset+buf.size-1</code>
+     * @param start index to check
+     * @param length number of elements in slice (must be &gt;0)
+     * @param stride in buffer elements between items of the slice
+     * @throws IndexOutOfBoundsException
      */
-    protected void checkInBuf(int i, int j) throws IndexOutOfBoundsException {
-        int a = buf.offset;
-        int b = a + buf.size - 1;
-        // Check: b >= i >= a and b >= j >= a. Cheat.
-        if (((i - a) | (j - a) | (b - i) | (b - j)) < 0) {
-            throw new IndexOutOfBoundsException();
+    protected void checkSlice(int start, int length, int stride) throws IndexOutOfBoundsException {
+        /*
+         * Simpler to check if we know which is the smaller of the two ends, which depends on the
+         * sign of the stride.
+         */
+        int lo, hi;
+        if (stride > 0) {
+            lo = start;
+            hi = start + (length - 1) * stride + 1;
+        } else {
+            hi = start;
+            lo = start + (length - 1) * stride + 1;
+        }
+        // Between the last element of the slice and the end of the buffer there are ...
+        int margin = shape[0] - hi;
+        if ((lo | margin) < 0) {
+            throw new IndexOutOfBoundsException("invalid slice of buffer");
         }
     }
 
     private static PyException bufferErrorFromSyndrome(int syndrome) {
 
         if ((syndrome & ND) != 0) {
-            return bufferRequires("shape");
+            return bufferRequires("shape array");
         } else if ((syndrome & STRIDES) != 0) {
-            return bufferRequires("strides");
+            return bufferRequires("strides array");
         } else if ((syndrome & INDIRECT) != 0) {
-            return bufferRequires("suboffsets");
+            return bufferRequires("suboffsets array");
         } else if ((syndrome & WRITABLE) != 0) {
             return bufferIsNot("writable");
         } else if ((syndrome & C_CONTIGUOUS) != 0) {
 
     /**
      * Convenience method to create (for the caller to throw) a
-     * <code>BufferError("underlying buffer requires {feature}")</code>.
+     * <code>BufferError("buffer structure requires consumer to use {feature}")</code>.
      *
      * @param feature
      * @return the error as a PyException
      */
     protected static PyException bufferRequires(String feature) {
-        return Py.BufferError("underlying buffer requires " + feature);
+        return Py.BufferError("buffer structure requires consumer to use " + feature);
     }
 
     /**

src/org/python/core/buffer/SimpleBuffer.java

 import org.python.core.BufferPointer;
 import org.python.core.PyBuffer;
 import org.python.core.PyException;
+import org.python.core.util.StringUtil;
 
 /**
  * Buffer API over a read-only one-dimensional array of one-byte items.
     public SimpleBuffer(int flags, byte[] storage, int offset, int size) throws PyException {
         this();
         // Wrap the exported data on a BufferPointer object
-        this.buf = new BufferPointer(storage, offset, size);
+        this.buf = new BufferPointer(storage, offset);
         this.shape[0] = size;        // Number of units in exported data
         checkRequestFlags(flags);    // Check request is compatible with type
     }
 
     @Override
     public PyBuffer getBufferSlice(int flags, int start, int length) {
-        // Translate relative to underlying buffer
-        int compIndex0 = buf.offset + start;
-        // Check the arguments define a slice within this buffer
-        checkInBuf(compIndex0, compIndex0 + length - 1);
-        // Create the slice from the sub-range of the buffer
-        return new SimpleView(getRoot(), flags, buf.storage, compIndex0, length);
+        if (length > 0) {
+            // Check the arguments define a slice within this buffer
+            checkSlice(start, length);
+            // Translate relative to underlying buffer
+            int compIndex0 = buf.offset + start;
+            // Create the slice from the sub-range of the buffer
+            return new SimpleView(getRoot(), flags, buf.storage, compIndex0, length);
+        } else {
+            // Special case for length==0 where above logic would fail. Efficient too.
+            return new ZeroByteBuffer.View(getRoot(), flags);
+        }
     }
 
     /**
     @Override
     public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {
 
-        if (stride == 1) {
+        if (stride == 1 || length < 2) {
             // Unstrided slice of simple buffer is itself simple
             return getBufferSlice(flags, start, length);
 
         } else {
+            // Check the arguments define a slice within this buffer
+            checkSlice(start, length, stride);
             // Translate relative to underlying buffer
             int compIndex0 = buf.offset + start;
-            // Check the slice sits within the present buffer (first and last indexes)
-            checkInBuf(compIndex0, compIndex0 + (length - 1) * stride);
             // Construct a view, taking a lock on the root object (this or this.root)
             return new Strided1DBuffer.SlicedView(getRoot(), flags, buf.storage, compIndex0,
-                                                  length, stride);
+                length, stride);
         }
     }
 
     @Override
     public BufferPointer getPointer(int index) {
-        return new BufferPointer(buf.storage, buf.offset + index, 1);
+        return new BufferPointer(buf.storage, buf.offset + index);
     }
 
     @Override
         return getPointer(indices[0]);
     }
 
+    @Override
+    public String toString() {
+        // For contiguous bytes in one dimension we can avoid the intAt() calls
+        return StringUtil.fromBytes(buf.storage, buf.offset, shape[0]);
+    }
+
     /**
      * A <code>SimpleBuffer.SimpleView</code> represents a contiguous subsequence of another
      * <code>SimpleBuffer</code>.

src/org/python/core/buffer/SimpleWritableBuffer.java

     public SimpleWritableBuffer(int flags, byte[] storage, int offset, int size) throws PyException {
         addFeatureFlags(WRITABLE);
         // Wrap the exported data on a BufferPointer object
-        this.buf = new BufferPointer(storage, offset, size);
+        this.buf = new BufferPointer(storage, offset);
         this.shape[0] = size;        // Number of units in exported data
         checkRequestFlags(flags);    // Check request is compatible with type
     }
     @Override
     public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException {
 
-        if (src.getLen() != buf.size) {
+        if (src.getLen() != getLen()) {
             throw differentStructure();
         }
 
      */
     @Override
     public PyBuffer getBufferSlice(int flags, int start, int length) {
-        // Translate relative to underlying buffer
-        int compIndex0 = buf.offset + start;
-        // Check the arguments define a slice within this buffer
-        checkInBuf(compIndex0, compIndex0 + length - 1);
-        // Create the slice from the sub-range of the buffer
-        return new SimpleView(getRoot(), flags, buf.storage, compIndex0, length);
+        if (length > 0) {
+            // Check the arguments define a slice within this buffer
+            checkSlice(start, length);
+            // Translate relative to underlying buffer
+            int compIndex0 = buf.offset + start;
+            // Create the slice from the sub-range of the buffer
+            return new SimpleView(getRoot(), flags, buf.storage, compIndex0, length);
+        } else {
+            // Special case for length==0 where above logic would fail. Efficient too.
+            return new ZeroByteBuffer.View(getRoot(), flags);
+        }
     }
 
     /**
      * <code>SimpleWritableBuffer</code> provides an implementation ensuring the returned slice is
      * writable.
      */
+    @Override
     public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {
 
-        if (stride == 1) {
+        if (stride == 1 || length < 2) {
             // Unstrided slice of simple buffer is itself simple
             return getBufferSlice(flags, start, length);
 
         } else {
+            // Check the arguments define a slice within this buffer
+            checkSlice(start, length, stride);
             // Translate relative to underlying buffer
             int compIndex0 = buf.offset + start;
-            // Check the slice sits within the present buffer (first and last indexes)
-            checkInBuf(compIndex0, compIndex0 + (length - 1) * stride);
             // Construct a view, taking a lock on the root object (this or this.root)
             return new Strided1DWritableBuffer.SlicedView(getRoot(), flags, buf.storage,
-                                                          compIndex0, length, stride);
+                compIndex0, length, stride);
         }
     }
 

src/org/python/core/buffer/Strided1DBuffer.java

     protected int stride;
 
     /**
-     * Absolute index in <code>buf.storage</code> of <code>item[0]</code>. For a positive
-     * <code>stride</code> this is equal to <code>buf.offset</code>, and for a negative
-     * <code>stride</code> it is <code>buf.offset+buf.size-1</code>. It has to be used in most of
-     * the places that buf.offset would appear in the index calculations of simpler buffers (that
-     * have unit stride).
-     */
-    protected int index0;
-
-    /**
      * Provide an instance of <code>Strided1DBuffer</code> with navigation variables partly
      * initialised, for sub-class use. To complete initialisation, the sub-class normally must
      * assign: {@link #buf}, {@link #shape}[0], and {@link #stride}, and call
      */
     public Strided1DBuffer(int flags, byte[] storage, int index0, int length, int stride)
             throws PyException {
-
         // Arguments programme the object directly
         this();
         this.shape[0] = length;
-        this.index0 = index0;
+        this.buf = new BufferPointer(storage, index0);
         this.stride = stride;
-
-        // Calculate buffer offset and size: start with distance of last item from first
-        int d = (length - 1) * stride;
-
-        if (stride >= 0) {
-            // Positive stride: indexing runs from first item
-            this.buf = new BufferPointer(storage, index0, 1 + d);
-            if (stride <= 1) {
-                // Really this is a simple buffer
-                addFeatureFlags(CONTIGUITY);
-            }
-        } else {
-            // Negative stride: indexing runs from last item
-            this.buf = new BufferPointer(storage, index0 + d, 1 - d);
+        if (stride == 1) {
+            // Really this is a simple buffer
+            addFeatureFlags(CONTIGUITY);
         }
-
         checkRequestFlags(flags);   // Check request is compatible with type
     }
 
 
     @Override
     public byte byteAt(int index) throws IndexOutOfBoundsException {
-        return buf.storage[index0 + index * stride];
+        return buf.storage[buf.offset + index * stride];
     }
 
     @Override
     protected int calcIndex(int index) throws IndexOutOfBoundsException {
-        return index0 + index * stride;
+        return buf.offset + index * stride;
     }
 
     @Override
     public void copyTo(int srcIndex, byte[] dest, int destPos, int length)
             throws IndexOutOfBoundsException {
         // Data is here in the buffers
-        int s = index0 + srcIndex * stride;
+        int s = buf.offset + srcIndex * stride;
         int d = destPos;
 
         // Strategy depends on whether items are laid end-to-end contiguously or there are gaps
             System.arraycopy(buf.storage, s, dest, d, length);
 
         } else {
-            // Discontiguous copy: single byte items
+            // Non-contiguous copy: single byte items
             int limit = s + length * stride;
             for (; s != limit; s += stride) {
                 dest[d++] = buf.storage[s];
      * which <i>x</i> was created from <i>u</i>. Thus <i>y(k) = u(r+sp+kmp)</i>, that is, the
      * composite offset is <i>r+sp</i> and the composite stride is <i>mp</i>.
      */
+    @Override
     public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {
 
-        // Translate relative to underlying buffer
-        int compStride = this.stride * stride;
-        int compIndex0 = index0 + start * stride;
+        if (length > 0) {
+            int compStride;
 
-        // Check the slice sits within the present buffer (first and last indexes)
-        checkInBuf(compIndex0, compIndex0 + (length - 1) * compStride);
+            if (stride == 1) {
+                // Check the arguments define a slice within this buffer
+                checkSlice(start, length);
+                // Composite stride is same as original stride
+                compStride = this.stride;
+            } else {
+                // Check the arguments define a slice within this buffer
+                checkSlice(start, length, stride);
+                // Composite stride is product
+                compStride = this.stride * stride;
+            }
 
-        // Construct a view, taking a lock on the root object (this or this.root)
-        return new SlicedView(getRoot(), flags, buf.storage, compIndex0, length, compStride);
+            // Translate start relative to underlying buffer
+            int compIndex0 = buf.offset + start * this.stride;
+            // Construct a view, taking a lock on the root object (this or this.root)
+            return new SlicedView(getRoot(), flags, buf.storage, compIndex0, length, compStride);
+
+        } else {
+            // Special case for length==0 where above logic would fail. Efficient too.
+            return new ZeroByteBuffer.View(getRoot(), flags);
+        }
     }
 
     @Override
     public BufferPointer getPointer(int index) {
-        return new BufferPointer(buf.storage, index0 + index, 1);
+        return new BufferPointer(buf.storage, buf.offset + index * stride);
     }
 
     @Override
     }
 
     /**
-     * A <code>Strided1DBuffer.SlicedView</code> represents a discontiguous subsequence of a simple
+     * A <code>Strided1DBuffer.SlicedView</code> represents a non-contiguous subsequence of a simple
      * buffer.
      */
     static class SlicedView extends Strided1DBuffer {

src/org/python/core/buffer/Strided1DWritableBuffer.java

      */
     public Strided1DWritableBuffer(int flags, byte[] storage, int index0, int length, int stride)
             throws PyException {
-
         // Arguments programme the object directly
         // this();
         this.shape[0] = length;
-        this.index0 = index0;
+        this.buf = new BufferPointer(storage, index0);
         this.stride = stride;
-
-        // Calculate buffer offset and size: start with distance of last item from first
-        int d = (length - 1) * stride;
-
-        if (stride >= 0) {
-            // Positive stride: indexing runs from first item
-            this.buf = new BufferPointer(storage, index0, 1 + d);
-            if (stride <= 1) {
-                // Really this is a simple buffer
-                addFeatureFlags(CONTIGUITY);
-            }
-        } else {
-            // Negative stride: indexing runs from last item
-            this.buf = new BufferPointer(storage, index0 + d, 1 - d);
+        if (stride == 1) {
+            // Really this is a simple buffer
+            addFeatureFlags(CONTIGUITY);
         }
-
+        addFeatureFlags(WRITABLE);
         checkRequestFlags(flags);   // Check request is compatible with type
     }
 
 
     @Override
     public void storeAt(byte value, int index) throws IndexOutOfBoundsException, PyException {
-        buf.storage[index0 + index * stride] = value;
+        buf.storage[buf.offset + index * stride] = value;
     }
 
     /**
 
         // Data is here in the buffers
         int s = srcPos;
-        int d = index0 + destIndex * stride;
+        int d = buf.offset + destIndex * stride;
 
         // Strategy depends on whether items are laid end-to-end or there are gaps
         if (stride == 1) {
             System.arraycopy(src, srcPos, buf.storage, d, length);
 
         } else {
-            // Discontiguous copy: single byte items
+            // Non-contiguous copy: single byte items
             int limit = d + length * stride;
             for (; d != limit; d += stride) {
                 buf.storage[d] = src[s++];
      * <code>Strided1DWritableBuffer</code> provides an implementation that returns a writable
      * slice.
      */
+    @Override
     public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {
 
-        // Translate relative to underlying buffer
-        int compStride = this.stride * stride;
-        int compIndex0 = index0 + start * stride;
+        if (length > 0) {
+            int compStride;
 
-        // Check the slice sits within the present buffer (first and last indexes)
-        checkInBuf(compIndex0, compIndex0 + (length - 1) * compStride);
+            if (stride == 1) {
+                // Check the arguments define a slice within this buffer
+                checkSlice(start, length);
+                // Composite stride is same as original stride
+                compStride = this.stride;
+            } else {
+                // Check the arguments define a slice within this buffer
+                checkSlice(start, length, stride);
+                // Composite stride is product
+                compStride = this.stride * stride;
+            }
 
-        // Construct a view, taking a lock on the root object (this or this.root)
-        return new SlicedView(getRoot(), flags, buf.storage, compIndex0, length, compStride);
+            // Translate start relative to underlying buffer
+            int compIndex0 = buf.offset + start * this.stride;
+            // Construct a view, taking a lock on the root object (this or this.root)
+            return new SlicedView(getRoot(), flags, buf.storage, compIndex0, length, compStride);
+
+        } else {
+            // Special case for length==0 where above logic would fail. Efficient too.
+            return new ZeroByteBuffer.View(getRoot(), flags);
+        }
     }
 
     /**
-     * A <code>Strided1DWritableBuffer.SlicedView</code> represents a discontiguous subsequence of a
-     * simple buffer.
+     * A <code>Strided1DWritableBuffer.SlicedView</code> represents a non-contiguous subsequence of
+     * a simple buffer.
      */
     static class SlicedView extends Strided1DWritableBuffer {
 

src/org/python/core/buffer/ZeroByteBuffer.java

+package org.python.core.buffer;
+
+import org.python.core.BufferPointer;
+import org.python.core.PyBuffer;
+import org.python.core.PyException;
+
+/**
+ * Buffer API over a zero length, one-dimensional array of one-byte items. The buffer is nominally
+ * writable, but since there is nowhere to write to, any attempt to write or read throws an
+ * <code>IndexOutOfBoundsException</code>. This class exists mostly to represent zero-length arrays,
+ * and particularly, zero-length slices for which implementations of
+ * {@link PyBuffer#getBufferSlice(int, int, int, int)} in any case need special logic. Bulk
+ * operations like {@link #copyTo(byte[], int)}) and {@link #toString()} efficiently do nothing,
+ * instead of calling complicated logic that finally does nothing.
+ */
+public class ZeroByteBuffer extends BaseBuffer {
+
+    /** Shared instance of a zero-length buffer. */
+    private static final BufferPointer EMPTY_BUF = new BufferPointer(new byte[0]);
+
+    /** Array containing a single zero for the length */
+    protected static final int[] SHAPE = {0};
+
+    /**
+     * Construct an instance of a zero-length buffer, choosing whether it should report itself to be
+     * read-only through {@link #isReadonly()}. This is moot, as any attempt to write to it produces
+     * an {@link IndexOutOfBoundsException}, but it is less surprising for client code that may ask,
+     * if the readability follows that of the object from which the buffer is derived.
+     *
+     * @param flags consumer requirements
+     * @param readonly set true if readonly
+     * @throws PyException (BufferError) when expectations do not correspond with the type
+     */
+    public ZeroByteBuffer(int flags, boolean readonly) throws PyException {
+        super(CONTIGUITY | SIMPLE | (readonly ? 0 : WRITABLE));
+        this.buf = EMPTY_BUF;                       // Wraps empty array
+        this.shape = SHAPE;                         // {0}
+        this.strides = SimpleBuffer.SIMPLE_STRIDES; // {1}
+        checkRequestFlags(flags);
+    }
+
+    @Override
+    public int getLen() {
+        return 0;
+    }
+
+    /**
+     * In a ZeroByteBuffer, the index is always out of bounds.
+     */
+    @Override
+    protected int calcIndex(int index) throws IndexOutOfBoundsException {
+        // This causes all access to the bytes in to throw (since BaseBuffer calls it).
+        throw new IndexOutOfBoundsException();
+    }
+
+    /**
+     * In a ZeroByteBuffer, if the dimensions are right, the index is out of bounds anyway.
+     */
+    @Override
+    protected int calcIndex(int... indices) throws IndexOutOfBoundsException {
+        // Bootless dimension check takes precedence (for consistency with other buffers)
+        checkDimension(indices);
+        // This causes all access to the bytes in to throw (since BaseBuffer calls it).
+        throw new IndexOutOfBoundsException();
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * In a ZeroByteBuffer, there is simply nothing to copy.
+     */
+    @Override
+    public void copyTo(byte[] dest, int destPos) throws IndexOutOfBoundsException {
+        // Nothing to copy
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * In a ZeroByteBuffer, there is simply nothing to copy.
+     */
+    @Override
+    public void copyTo(int srcIndex, byte[] dest, int destPos, int length)
+        throws IndexOutOfBoundsException, PyException {
+        // Nothing to copy
+    }
+
+    /**
+     * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source length
+     * is zero.
+     */
+    @Override
+    public void copyFrom(byte[] src, int srcPos, int destIndex, int length)
+        throws IndexOutOfBoundsException, PyException {
+        if (length > 0) {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    /**
+     * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source length
+     * is zero.
+     */
+    @Override
+    public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException {
+        if (src.getLen() > 0) {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    /**
+     * Only a zero-length slice at zero is valid (in which case, the present buffer will do nicely
+     * as a result, with the export count incremented.
+     */
+    @Override
+    public PyBuffer getBufferSlice(int flags, int start, int length) {
+        if (start == 0 && length <= 0) {
+            return this.getBuffer(flags);
+        } else {
+            throw new IndexOutOfBoundsException();
+        }
+    }
+
+    /**
+     * Only a zero-length slice at zero is valid (in which case, the present buffer will do nicely
+     * as a result, with the export count incremented.
+     */
+    @Override
+    public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {
+        // It can't matter what the stride is since length is zero, or there's an error.
+        return getBufferSlice(flags, start, length);
+    }
+
+    /**
+     * For a ZeroByteBuffer, it's the empty string.
+     */
+    @Override
+    public String toString() {
+        return "";
+    }
+
+    /**
+     * A <code>ZeroByteBuffer.View</code> represents a contiguous subsequence of another
+     * <code>PyBuffer</code>. We don't need it to make slices of the ZeroByteBuffer itself, but it
+     * is useful for making zero-length slices of anything else. Lock-release semantics must still
+     * be observed. In Python, a zero-length slice obtained from the memoryview of a bytearray still
+     * counts as an export from the bytearray.
+     */
+    static class View extends ZeroByteBuffer {
+
+        /** The buffer on which this is a slice view */
+        PyBuffer root;
+
+        /**
+         * Construct a slice of a ZeroByteBuffer, which it goes without saying is of zero length at
+         * position zero.
+         *
+         * @param root buffer which will be acquired and must be released ultimately
+         * @param flags the request flags of the consumer that requested the slice
+         */
+        public View(PyBuffer root, int flags) {
+            // Create a new ZeroByteBuffer on who-cares-what byte array
+            super(flags, root.isReadonly());
+            // But we still have to get a lease on the root PyBuffer
+            this.root = root.getBuffer(FULL_RO);
+        }
+
+        @Override
+        protected PyBuffer getRoot() {
+            return root;
+        }
+
+        @Override
+        public void releaseAction() {
+            // We have to release the root too if ours was final.
+            root.release();
+        }
+  }
+}

src/org/python/core/util/StringUtil.java

 
     /**
      * Return a new String with chars corresponding to buf, which is a byte-oriented buffer obtained
-     * through the buffer API.
-     * 
+     * through the buffer API. It depends on the implementation of {@link PyBuffer#toString()}
+     * provided by each buffer implementation.
+     *
      * @param buf a PyBuffer of bytes
      * @return a new String corresponding to the bytes in buf
      */
     public static String fromBytes(PyBuffer buf) {
-        BufferPointer bp = buf.getBuf();
-        return fromBytes(bp.storage, bp.offset, bp.size);
+        return buf.toString();
     }
 
     /**

tests/java/org/python/core/PyBufferTest.java

 import java.lang.ref.SoftReference;
 import java.lang.ref.WeakReference;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
 
 import junit.framework.TestCase;
 
+import org.python.core.buffer.BaseBuffer;
 import org.python.core.buffer.SimpleBuffer;
 import org.python.core.buffer.SimpleStringBuffer;
 import org.python.core.buffer.SimpleWritableBuffer;
  * exception.
  * <p>
  * The Jython buffer API follows the structures of the CPython buffer API so that it supports in
- * principle the use of multi-dimensional, strided add indirect array structures as buffers.
+ * principle the use of multi-dimensional, strided and indirect array structures as buffers.
  * However, actual buffers in the Jython core, and therefore these tests, limit themselves to one
- * dimensional contiguous buffers with a simple organisation. Some tests apply directly to the
+ * dimensional (possibly non-contiguous) directly-indexed buffers. Some tests apply directly to the
  * N-dimensional cases, and some need a complete re-think. Sub-classing this test would probably be
  * a good way to extend it to a wider range.
  */
 public class PyBufferTest extends TestCase {
 
+    /** Control amount of output. Instance variable so can be adjusted temporarily per test. */
+    protected int verbosity = 0;
+
     /**
      * Generated constructor
      *
     /*
      * Values for initialising the exporters.
      */
-    private static final ByteMaterial byteMaterial = new ByteMaterial(0, 17, 16);
-    private static final ByteMaterial abcMaterial = new ByteMaterial("abcdef");
+    private static final ByteMaterial byteMaterial = new ByteMaterial(0, 16, 17);
+    private static final ByteMaterial abcMaterial = new ByteMaterial("abcdefgh");
     private static final ByteMaterial stringMaterial = new ByteMaterial("Mon côté fâcheux");
     private static final ByteMaterial emptyMaterial = new ByteMaterial(new byte[0]);
-    private static final ByteMaterial longMaterial = new ByteMaterial(0, 5, 1000);
+    public static final int LONG = 1000;
+    private static final ByteMaterial longMaterial = new ByteMaterial(0, LONG, 5);
 
+    @Override
     protected void setUp() throws Exception {
         super.setUp();
 
         // Exception raising requires the Jython interpreter
         interp = new PythonInterpreter();
 
-        // Tests using local examples
-        queueWrite(new SimpleWritableExporter(abcMaterial.getBytes()), abcMaterial);
-        queueReadonly(new SimpleExporter(byteMaterial.getBytes()), byteMaterial);
-        queueReadonly(new StringExporter(stringMaterial.string), stringMaterial);
-        queueWrite(new SimpleWritableExporter(emptyMaterial.getBytes()), emptyMaterial);
+        // Tests using local types of exporter
+        genWritable(new SimpleWritableExporter(abcMaterial.getBytes()), abcMaterial);
+        genReadonly(new SimpleExporter(byteMaterial.getBytes()), byteMaterial);
+        genReadonly(new StringExporter(stringMaterial.string), stringMaterial);
+        genWritable(new SimpleWritableExporter(emptyMaterial.getBytes()), emptyMaterial);
 
         // Tests with PyByteArray
-        queueWrite(new PyByteArray(abcMaterial.getBytes()), abcMaterial);
-        queueWrite(new PyByteArray(longMaterial.getBytes()), longMaterial);
-        queueWrite(new PyByteArray(), emptyMaterial);
+        genWritable(new PyByteArray(abcMaterial.getBytes()), abcMaterial);
+        genWritable(new PyByteArray(longMaterial.getBytes()), longMaterial);
+        genWritable(new PyByteArray(), emptyMaterial);
 
         // Tests with PyString
-        queueReadonly(new PyString(abcMaterial.string), abcMaterial);
-        queueReadonly(new PyString(), emptyMaterial);
+        genReadonly(new PyString(abcMaterial.string), abcMaterial);
+        genReadonly(new PyString(), emptyMaterial);
 
         // Ensure case is tested where PyByteArray has an internal offset
         PyByteArray truncated = new PyByteArray(stringMaterial.getBytes());
         truncated.delRange(0, 4);
         ByteMaterial truncatedMaterial = new ByteMaterial(stringMaterial.string.substring(4));
         assert truncated.__alloc__() > truncatedMaterial.length;
-        queueWrite(truncated, truncatedMaterial);
+        genWritable(truncated, truncatedMaterial);
     }
 
-    private void queueWrite(BufferProtocol exporter, ByteMaterial material) {
-        BufferTestPair pair = new BufferTestPair(exporter, material);
-        buffersToRead.add(pair);
-        buffersToWrite.add(pair);
+    /** Generate a series of test material for a writable object. */
+    private void genWritable(BufferProtocol exporter, ByteMaterial material) {
+        generate(exporter, material, false);
     }
 
-    private void queueReadonly(BufferProtocol exporter, ByteMaterial material) {
-        BufferTestPair pair = new BufferTestPair(exporter, material);
+    /** Generate a series of test material for a read-only object. */
+    private void genReadonly(BufferProtocol exporter, ByteMaterial material) {
+        generate(exporter, material, true);
+    }
+
+    /** Lengths we will use if we can when slicing view */
+    private static final int[] sliceLengths = {1, 2, 5, 0, LONG / 4};
+
+    /** Step sizes we will use if we can when slicing view */
+    private static final int[] sliceSteps = {1, 2, 3, 7};
+
+    /**
+     * Generate a series of test material for a read-only or writable object. Given one exporter,
+     * and its reference ByteMaterial this method first queues a BufferTestPair corresponding to the
+     * exporter as the test subject and its test material. This provides a "direct" PyBuffer view on
+     * the exporter. It then goes on to make a variety of sliced PyBuffer views of the exporter by
+     * calling {@link PyBuffer#getBufferSlice(int, int, int, int)} on the direct view. The slices
+     * are made with a variety of argument combinations, filtered down to those that make sense for
+     * the size of the direct view. Each sliced buffer (considered a test subject now), together
+     * with correspondingly sliced reference ByteMaterial is queued as BufferTestPair.
+     *
+     * @param exporter underlying object
+     * @param material reference material corresponding to the exporter
+     * @param readonly whether the exporter is of read-only type
+     */
+    private void generate(BufferProtocol exporter, ByteMaterial material, boolean readonly) {
+
+        // Generate a test using the buffer directly exported by the exporter
+        PyBuffer direct = queue(exporter, material, readonly);
+
+        // Generate some slices from the material and this direct view
+        int N = material.length;
+        int M = (N + 4) / 4;    // At least one and about N/4
+
+        // For a range of start positions up to one beyond the end
+        for (int start = 0; start <= N; start += M) {
+            // For a range of lengths
+            for (int length : sliceLengths) {
+
+                if (length == 0) {
+                    queue(direct, material, start, 0, 1, readonly);
+                    queue(direct, material, start, 0, 2, readonly);
+
+                } else if (length == 1 && start < N) {
+                    queue(direct, material, start, 1, 1, readonly);
+                    queue(direct, material, start, 1, 2, readonly);
+
+                } else if (start < N) {
+
+                    // And for a range of step sizes
+                    for (int step : sliceSteps) {
+                        // Check this is a feasible slice
+                        if (start + (length - 1) * step < N) {
+                            queue(direct, material, start, length, step, readonly);
+                        }
+                    }
+
+                    // Now use all the step sizes negatively
+                    for (int step : sliceSteps) {
+                        // Check this is a feasible slice
+                        if (start - (length - 1) * step >= 0) {
+                            queue(direct, material, start, length, -step, readonly);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /** Generate and queue one test of non-slice type (if getting a buffer succeeds). */
+    private PyBuffer queue(BufferProtocol exporter, ByteMaterial material, boolean readonly) {
+        if (verbosity > 2) {
+            System.out.printf("queue non-slice: length=%d, readonly=%s\n", material.length,
+                    readonly);
+        }
+        BufferTestPair pair = new BufferTestPair(exporter, material, readonly);
+        queue(pair);
+        return pair.view;
+    }
+
+    /** Generate and queue one test of slice type (if getting a buffer succeeds). */
+    private PyBuffer queue(PyBuffer direct, ByteMaterial material, int start, int length, int step,
+            boolean readonly) {
+
+        int flags = readonly ? PyBUF.FULL_RO : PyBUF.FULL;
+        PyBuffer subject = null;
+
+        /*
+         * Make a slice. We ignore this case if we fail, because we are not testing slice creation
+         * here, but making slices to be tested as buffers. We'll test slice creation in
+         * testGetBufferSlice.
+         */
+        try {
+            if (verbosity > 2) {
+                System.out.printf("  queue slice: start=%4d, length=%4d, step=%4d\n", start,
+                        length, step);
+            }
+            subject = direct.getBufferSlice(flags, start, length, step);
+            ByteMaterial sliceMaterial = material.slice(start, length, step);
+            BufferTestPair pair = new BufferTestPair(subject, sliceMaterial, step, readonly);
+            queue(pair);
+        } catch (Exception e) {
+            /*
+             * We ignore this case if we fail, because we are not testing slice creation here, but
+             * making slices to be tested as buffers. We'll test slice creation elsewhere.
+             */
+            if (verbosity > 2) {
+                System.out.printf("*** SKIP %s\n", e);
+            }
+        }
+
+        return subject;
+    }
+
+    /** Queue one instance of test material for a read-only or writable object. */
+    private void queue(BufferTestPair pair) {
         buffersToRead.add(pair);
-        buffersToFailToWrite.add(pair);
+        if (pair.readonly) {
+            buffersToFailToWrite.add(pair);
+        } else {
+            buffersToWrite.add(pair);
+        }
     }
 
     /** Read operations should succeed on all these objects. */
     /** Write operations should fail on all these objects. */
     private List<BufferTestPair> buffersToFailToWrite = new LinkedList<BufferTestPair>();
 
-    /** We should be able to get a buffer for all these flag types. */
-    private int[] validFlags = {PyBUF.SIMPLE, PyBUF.ND, PyBUF.STRIDES, PyBUF.INDIRECT};
+    /**
+     * A one-dimensional exporter should be able to give us a buffer for all these flag types.
+     */
+    private static final int[] simpleFlags = {PyBUF.SIMPLE, PyBUF.ND, PyBUF.STRIDES,
+            PyBUF.INDIRECT, PyBUF.FULL_RO};
 
-    /** To which we can add any of these (in one dimension, anyway) */
-    private int[] validTassles = {0,
-                                  PyBUF.FORMAT,
-                                  PyBUF.C_CONTIGUOUS,
-                                  PyBUF.F_CONTIGUOUS,
-                                  PyBUF.ANY_CONTIGUOUS};
+    /** To {@link #simpleFlags} we can add any of these */
+    private static final int[] simpleTassles = {0, PyBUF.FORMAT, PyBUF.C_CONTIGUOUS,
+            PyBUF.F_CONTIGUOUS, PyBUF.ANY_CONTIGUOUS};
 
     /**
-     * Test method for {@link org.python.core.BufferProtocol#getBuffer()}.
+     * A one-dimensional exporter with stride!=1 is restricted to give us a buffer only for these
+     * flag types.
      */
-    public void testExporterGetBuffer() {
+    private static final int[] strided1DFlags = {PyBUF.STRIDES, PyBUF.INDIRECT, PyBUF.FULL_RO};
 
-        for (BufferTestPair test : buffersToRead) {
-            System.out.println("getBuffer(): " + test);
-            for (int flags : validFlags) {
-                for (int tassle : validTassles) {
-                    PyBuffer view = test.exporter.getBuffer(flags | tassle);
-                    assertNotNull(view);
-                }
-            }
-        }
-
-        for (BufferTestPair test : buffersToWrite) {
-            System.out.println("getBuffer(WRITABLE): " + test);
-            for (int flags : validFlags) {
-                for (int tassle : validTassles) {
-                    PyBuffer view = test.exporter.getBuffer(flags | tassle | PyBUF.WRITABLE);
-                    assertNotNull(view);
-                }
-            }
-        }
-
-        for (BufferTestPair test : buffersToFailToWrite) {
-            System.out.println("getBuffer(WRITABLE): " + test);
-            for (int flags : validFlags) {
-                try {
-                    test.exporter.getBuffer(flags | PyBUF.WRITABLE);
-                    fail("Write access not prevented: " + test);
-                } catch (PyException pye) {
-                    // Expect BufferError
-                    assertEquals(Py.BufferError, pye.type);
-                }
-            }
-        }
-
-    }
+    /** To {@link #strided1DFlags} we can add any of these */
+    private static final int[] strided1DTassles = {0, PyBUF.FORMAT};
 
     /**
      * Test method for {@link org.python.core.PyBUF#isReadonly()}.
     public void testIsReadonly() {
 
         for (BufferTestPair test : buffersToWrite) {
-            System.out.println("isReadonly: " + test);
-            assertFalse(test.simple.isReadonly());
+            if (verbosity > 0) {
+                System.out.println("isReadonly: " + test);
+            }
+            assertFalse(test.view.isReadonly());
         }
 
         for (BufferTestPair test : buffersToFailToWrite) {
-            System.out.println("isReadonly: " + test);
-            assertTrue(test.simple.isReadonly());
+            if (verbosity > 0) {
+                System.out.println("isReadonly: " + test);
+            }
+            assertTrue(test.view.isReadonly());
         }
     }
 
      */
     public void testGetNdim() {
         for (BufferTestPair test : buffersToRead) {
-            System.out.println("getNdim: " + test);
-            assertEquals("simple ndim", test.shape.length, test.simple.getNdim());
+            if (verbosity > 0) {
+                System.out.println("getNdim: " + test);
+            }
+            assertEquals("unexpected ndim", test.shape.length, test.view.getNdim());
         }
     }
 
      */
     public void testGetShape() {
         for (BufferTestPair test : buffersToRead) {
-            System.out.println("getShape: " + test);
-            int[] shape = test.simple.getShape();
-            assertNotNull(shape);
-            assertIntsEqual("simple shape", test.shape, shape);
+            if (verbosity > 0) {
+                System.out.println("getShape: " + test);
+            }
+            int[] shape = test.view.getShape();
+            assertNotNull("shape[] should always be provided", shape);
+            assertIntsEqual("unexpected shape", test.shape, shape);
         }
     }
 
      */
     public void testGetLen() {
         for (BufferTestPair test : buffersToRead) {
-            System.out.println("getLen: " + test);
-            assertEquals(" simple len", test.material.bytes.length, test.simple.getLen());
-            assertEquals("strided len", test.material.bytes.length, test.strided.getLen());
+            if (verbosity > 0) {
+                System.out.println("getLen: " + test);
+            }
+            assertEquals("unexpected length", test.material.length, test.view.getLen());
         }
     }
 
      */
     public void testByteAt() {
         for (BufferTestPair test : buffersToRead) {
-            System.out.println("byteAt: " + test);
+            if (verbosity > 0) {
+                System.out.println("byteAt: " + test);
+            }
             int n = test.material.length;
             byte[] exp = test.material.bytes;
             for (int i = 0; i < n; i++) {
-                assertEquals(exp[i], test.simple.byteAt(i));
+                assertEquals(exp[i], test.view.byteAt(i));
             }
         }
     }
     public void testByteAtNdim() {
         int[] index = new int[1];
         for (BufferTestPair test : buffersToRead) {
-            System.out.println("byteAt(array): " + test);
-            if (test.strided.getShape().length != 1) {
-                fail("Test not implemented dimensions != 1");
+            if (verbosity > 0) {
+                System.out.println("byteAt(array): " + test);
+            }
+            if (test.view.getShape().length != 1) {
+                fail("Test not implemented if dimensions != 1");
             }
             byte[] exp = test.material.bytes;
             int n = test.material.length;
-            // Run through 1D index for simple
+            // Run through 1D index for view
             for (int i = 0; i < n; i++) {
                 index[0] = i;
-                assertEquals(exp[i], test.simple.byteAt(index));
+                assertEquals(exp[i], test.view.byteAt(index));
             }
+
             // Check 2D index throws
             try {
-                test.simple.byteAt(0, 0);
+                test.view.byteAt(0, 0);
                 fail("Use of 2D index did not raise exception");
             } catch (PyException pye) {
                 // Expect BufferError
      */
     public void testIntAt() {
         for (BufferTestPair test : buffersToRead) {
-            System.out.println("intAt: " + test);
+            if (verbosity > 0) {
+                System.out.println("intAt: " + test);
+            }
             int n = test.material.length;
             int[] exp = test.material.ints;
             for (int i = 0; i < n; i++) {
-                assertEquals(exp[i], test.simple.intAt(i));
+                assertEquals(exp[i], test.view.intAt(i));
             }
         }
     }
     public void testIntAtNdim() {
         int[] index = new int[1];
         for (BufferTestPair test : buffersToRead) {
-            System.out.println("intAt(array): " + test);
-            if (test.strided.getShape().length != 1) {
-                fail("Test not implemented dimensions != 1");
+            if (verbosity > 0) {
+                System.out.println("intAt(array): " + test);
+            }
+            if (test.view.getShape().length != 1) {
+                fail("Test not implemented for dimensions != 1");
             }
             int[] exp = test.material.ints;
             int n = test.material.length;
-            // Run through 1D index for simple
+            // Run through 1D index for view
             for (int i = 0; i < n; i++) {
                 index[0] = i;
-                assertEquals(exp[i], test.simple.intAt(index));
+                assertEquals(exp[i], test.view.intAt(index));
             }
             // Check 2D index throws
             try {
-                test.simple.intAt(0, 0);
+                test.view.intAt(0, 0);
                 fail("Use of 2D index did not raise exception");
             } catch (PyException pye) {
                 // Expect BufferError
      */
     public void testStoreAt() {
         for (BufferTestPair test : buffersToWrite) {
-            System.out.println("storeAt: " + test);
+            if (verbosity > 0) {
+                System.out.println("storeAt: " + test);
+            }
             int n = test.material.length;
             int[] exp = test.material.ints;
             // Write modified test material into each location using storeAt()
             for (int i = 0; i < n; i++) {
                 byte v = (byte)(exp[i] ^ 3);    // twiddle some bits
-                test.simple.storeAt(v, i);
+                test.view.storeAt(v, i);
             }
             // Compare each location with modified test data using intAt()
             for (int i = 0; i < n; i++) {
-                assertEquals(exp[i] ^ 3, test.simple.intAt(i));
+                assertEquals(exp[i] ^ 3, test.view.intAt(i));
             }
         }
     }
      */
     public void testStoreAtNdim() {
         for (BufferTestPair test : buffersToWrite) {
-            System.out.println("storeAt: " + test);
+            if (verbosity > 0) {
+                System.out.println("storeAt: " + test);
+            }
             int n = test.material.length;
             int[] exp = test.material.ints;
             // Write modified test material into each location using storeAt()
             for (int i = 0; i < n; i++) {
                 byte v = (byte)(exp[i] ^ 3);    // twiddle some bits
-                test.simple.storeAt(v, i);
+                test.view.storeAt(v, i);
             }
             // Compare each location with modified test data using intAt()
             for (int i = 0; i < n; i++) {
-                assertEquals(exp[i] ^ 3, test.simple.intAt(i));
+                assertEquals(exp[i] ^ 3, test.view.intAt(i));
             }
             // Check 2D index throws
             try {
-                test.simple.storeAt((byte)1, 0, 0);
+                test.view.storeAt((byte)1, 0, 0);
                 fail("Use of 2D index did not raise exception");
             } catch (PyException pye) {
                 // Expect BufferError
     public void testCopyTo() {
         final int OFFSET = 5;
         for (BufferTestPair test : buffersToRead) {
-            System.out.println("copyTo: " + test);
+            if (verbosity > 0) {
+                System.out.println("copyTo: " + test);
+            }
             int n = test.material.length;
             // Try with zero offset
             byte[] actual = new byte[n];
-            test.simple.copyTo(actual, 0);
+            test.view.copyTo(actual, 0);
             assertBytesEqual("copyTo() incorrect", test.material.bytes, actual, 0);
             // Try to middle of array
             actual = new byte[n + 2 * OFFSET];
-            test.simple.copyTo(actual, OFFSET);
+            test.view.copyTo(actual, OFFSET);
             assertBytesEqual("copyTo(offset) incorrect", test.material.bytes, actual, OFFSET);
             assertEquals("data before destination", 0, actual[OFFSET - 1]);
             assertEquals("data after destination", 0, actual[OFFSET + n]);
         final byte BLANK = 7;
 
         for (BufferTestPair test : buffersToRead) {
-            System.out.println("copyTo(from slice): " + test);
-            PyBuffer view = test.simple;
+            if (verbosity > 0) {
+                System.out.println("copyTo(from slice): " + test);
+            }
+            PyBuffer view = test.view;
 
             int n = test.material.length;
             byte[] actual = new byte[n + 2 * OFFSET];
 
                     // A variety of lengths from zero to (n-srcIndex)-ish
                     for (int length = 0; srcIndex + length <= n; length = 2 * length + 1) {
-                        /*
-                         * System.out.printf("  copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n",
-                         * srcIndex, srcIndex + length, n, destPos, destPos + length,
-                         * actual.length);
-                         */
+
+                        if (verbosity > 1) {
+                            System.out.printf("  copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n",
+                                    srcIndex, srcIndex + length, n, destPos, destPos + length,
+                                    actual.length);
+                        }
+
                         Arrays.fill(actual, BLANK);
 
                         // Test the method
 
                         // Check changed part of destination
                         assertBytesEqual("copyTo(slice) incorrect", test.material.bytes, srcIndex,
-                                         actual, destPos, length);
+                                actual, destPos, length);
                         if (destPos > 0) {
                             assertEquals("data before destination", BLANK, actual[destPos - 1]);
                         }
                     // And from exactly n-srcIndex down to zero-ish
                     for (int trim = 0; srcIndex + trim <= n; trim = 2 * trim + 1) {
                         int length = n - srcIndex - trim;
-                        /*
-                         * System.out.printf("  copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n",
-                         * srcIndex, srcIndex + length, n, destPos, destPos + length,
-                         * actual.length);
-                         */
+
+                        if (verbosity > 1) {
+                            System.out.printf("  copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n",
+                                    srcIndex, srcIndex + length, n, destPos, destPos + length,
+                                    actual.length);
+                        }
+
                         Arrays.fill(actual, BLANK);
 
                         // Test the method
 
                         // Check changed part of destination
                         assertBytesEqual("copyTo(slice) incorrect", test.material.bytes, srcIndex,
-                                         actual, destPos, length);
+                                actual, destPos, length);
                         if (destPos > 0) {
                             assertEquals("data before destination", BLANK, actual[destPos - 1]);
                         }
         final byte BLANK = 7;
 
         for (BufferTestPair test : buffersToWrite) {
-            System.out.println("copyFrom(): " + test);
-            PyBuffer view = test.simple;
+            if (verbosity > 0) {
+                System.out.println("copyFrom(): " + test);
+            }
+            PyBuffer view = test.view;
 
             int n = test.material.length;
             byte[] actual = new byte[n];
                     // A variety of lengths from zero to (n-destIndex)-ish
                     for (int length = 0; destIndex + length <= n; length = 2 * length + 1) {
 
-                        // System.out.printf("  copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", srcPos,
-                        // srcPos + length, n, destIndex, destIndex + length,
-                        // actual.length);
+                        if (verbosity > 1) {
+                            System.out.printf("  copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n",
+                                    srcPos, srcPos + length, n, destIndex, destIndex + length,
+                                    actual.length);
+                        }
 
                         // Initialise the object (have to do each time) and expected value
                         for (int i = 0; i < n; i++) {
                     for (int trim = 0; destIndex + trim <= n; trim = 2 * trim + 1) {
                         int length = n - destIndex - trim;
 
-                        // System.out.printf("  copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", srcPos,
-                        // srcPos + length, n, destIndex, destIndex + length,
-                        // actual.length);
+                        if (verbosity > 1) {
+                            System.out.printf("  copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n",
+                                    srcPos, srcPos + length, n, destIndex, destIndex + length,
+                                    actual.length);
+                        }
 
                         // Initialise the object (have to do each time) and expected value
                         for (int i = 0; i < n; i++) {
     }
 
     /**
+     * Test method for {@link org.python.core.BufferProtocol#getBuffer()} and
+     * {@link org.python.core.PyBuffer#getBuffer()}.
+     */
+    public void testGetBuffer() {
+
+        for (BufferTestPair test : buffersToRead) {
+            if (verbosity > 0) {
+                System.out.println("getBuffer(): " + test);
+            }
+            for (int flags : test.validFlags) {
+                for (int tassle : test.validTassles) {
+                    PyBuffer view = test.subject.getBuffer(flags | tassle);
+                    assertNotNull(view);
+                }
+            }
+        }
+
+        for (BufferTestPair test : buffersToWrite) {
+            if (verbosity > 0) {
+                System.out.println("getBuffer(WRITABLE): " + test);
+            }
+            for (int flags : test.validFlags) {
+                for (int tassle : test.validTassles) {
+                    PyBuffer view = test.subject.getBuffer(flags | tassle | PyBUF.WRITABLE);
+                    assertNotNull(view);
+                }
+            }
+        }
+
+        for (BufferTestPair test : buffersToFailToWrite) {
+            if (verbosity > 0) {
+                System.out.println("getBuffer(WRITABLE): " + test);
+            }
+            for (int flags : test.validFlags) {
+                try {
+                    test.subject.getBuffer(flags | PyBUF.WRITABLE);
+                    fail("Write access not prevented: " + test);
+                } catch (PyException pye) {
+                    // Expect BufferError
+                    assertEquals(Py.BufferError, pye.type);
+                }
+            }
+        }
+
+    }
+
+    /**
+     * Test method for {@link org.python.core.PyBUF#release()}, exercising the release semantics of
+     * PyBuffer.
+     */
+    public void testRelease() {
+
+        /*
+         * Testing the semantics of release() is tricky when it comes to 'final' release behaviour.
+         * We'd like to test that buffers can be acquired and released, that "over release" is
+         * detected as an error, and that after final release of the buffer (where the export count
+         * becomes zero) an exporter remains capable of exporting again. Each test is constructed
+         * with a subject and a view on the subject (if the subject is an exporter), so you might
+         * think the export count would be one in every case. Two problems: in many tests, the
+         * subject is a PyBuffer, which has the option (if it would work) to return itself; and a
+         * PyBuffer is not expected to provide a new buffer view once finally released.
+         */
+
+        Set<PyBuffer> uniqueBuffers = new HashSet<PyBuffer>();
+
+        for (BufferTestPair test : buffersToRead) {
+            // Test a pattern of acquire and release with one more release than acquire
+            doTestRelease(test);
+            uniqueBuffers.add(test.view);
+        }
+
+        // All buffers are released: test that any further release is detected as an error.
+        for (PyBuffer view : uniqueBuffers) {
+            doTestOverRelease(view);
+        }
+
+        // All exporters are currently not exporting buffers
+        for (BufferTestPair test : buffersToRead) {
+            if (!(test.subject instanceof PyBuffer)) {
+                doTestGetAfterRelease(test);
+            }
+        }
+    }
+
+    /**
+     * Exercise the release semantics of one BufferTestPair. At the end, the view in the
+     * BufferTestPair should be fully released, ({@link PyBuffer#isReleased()}<code>==true</code>).
+     */
+    private void doTestRelease(BufferTestPair test) {
+
+        if (verbosity > 0) {
+            System.out.println("release: " + test);
+        }
+        int flags = PyBUF.STRIDES | PyBUF.FORMAT;
+        BufferProtocol sub = test.subject;
+
+        // The object will be exporting test.view and N other views we don't know about
+        PyBuffer a = test.view;                     // = N+1 exports
+        PyBuffer b = sub.getBuffer(PyBUF.FULL_RO);  // = N+2 export
+        PyBuffer c = sub.getBuffer(flags);          // = N+3 exports
+        checkExporting(sub);
+
+        // Now see that releasing in some other order works correctly
+        b.release();                                // = N+2 exports
+        a.release();                                // = N+1 exports
+        checkExporting(sub);
+
+        // You can get a buffer from a buffer (c is unreleased)
+        PyBuffer d = c.getBuffer(flags);            // = N+2 exports
+        c.release();                                // = N+1 export
+        checkExporting(sub);
+        d.release();                                // = N exports
+    }
+
+    /**
+     * The view argument should be a fully released buffer, ({@link PyBuffer#isReleased()}
+     * <code>==true</code>). We check that further releases raise an error.
+     */
+    private void doTestOverRelease(PyBuffer view) {
+
+        // Was it released finally?
+        assertTrue("Buffer not finally released as expected", view.isReleased());
+
+        // Further releases are an error
+        try {
+            view.release();                        // = -1 exports (oops)
+            fail("excess release not detected");
+        } catch (Exception e) {
+            // Success
+        }
+
+    }
+
+    /**
+     * The test in the argument is one where the subject is a real object (not another buffer) from
+     * which all buffer views should have been released in {@link #doTestRelease(BufferTestPair)}.
+     * We check this is true, and that a new buffer may still be acquired from the real object, but
+     * not from the released buffer.
+     */
+    private void doTestGetAfterRelease(BufferTestPair test) {
+
+        if (verbosity > 0) {
+            System.out.println("get again: " + test);
+        }
+        BufferProtocol sub = test.subject;
+
+        // Fail here if doTestRelease did not fully release, or
+        checkNotExporting(sub);
+
+        // Further gets via the released buffer are an error
+        try {
+            test.view.getBuffer(PyBUF.FULL_RO);
+            fail("PyBuffer.getBuffer after final release not detected");
+        } catch (Exception e) {
+            // Detected *and* prevented?
+            checkNotExporting(sub);
+        }
+
+        // And so are sliced gets
+        try {
+            test.view.getBufferSlice(PyBUF.FULL_RO, 0, 0);
+            fail("PyBuffer.getBufferSlice after final release not detected");
+        } catch (Exception e) {
+            // Detected *and* prevented?
+            checkNotExporting(sub);
+        }
+
+        /*
+         * Even after some abuse, we can still get and release a buffer.
+         */
+        PyBuffer b = sub.getBuffer(PyBUF.FULL_RO);      // = 1 export
+        checkExporting(sub);
+        b.release();                                    // = 0 exports
+        checkNotExporting(sub);
+    }
+
+    /**
+     * Error if subject is a PyBuffer and is released, or is a real exporter that (we can tell) is
+     * not actually exporting.
+     *
+     * @param subject
+     */
+    private void checkExporting(BufferProtocol subject) {
+        if (subject instanceof TestableExporter) {
+            assertTrue("exports not being counted", ((TestableExporter)subject).isExporting());
+        } else if (subject instanceof PyBuffer) {
+            assertFalse("exports not being counted (PyBuffer)", ((PyBuffer)subject).isReleased());
+        } else if (subject instanceof PyByteArray) {
+            // Size-changing access should fail
+            try {
+                ((PyByteArray)subject).bytearray_extend(Py.One); // Appends one zero byte
+                fail("bytearray_extend with exports should fail");
+            } catch (Exception e) {
+                // Success
+            }
+        }
+        // Other types cannot be checked
+    }
+
+    /**
+     * Error if subject is a PyBuffer that is released, or is a real exporter (that we can tell) is
+     * locked.
+     *
+     * @param subject
+     */
+    private void checkNotExporting(BufferProtocol subject) {
+        if (subject instanceof TestableExporter) {
+            assertFalse("exports counted incorrectly", ((TestableExporter)subject).isExporting());
+        } else if (subject instanceof PyBuffer) {
+            assertTrue("exports counted incorrectly (PyBuffer)", ((PyBuffer)subject).isReleased());
+        } else if (subject instanceof PyByteArray) {
+            // Size-changing access should succeed
+            try {
+                PyByteArray sub = ((PyByteArray)subject);
+                sub.bytearray_extend(Py.One);
+                sub.del(sub.__len__() - 1);
+            } catch (Exception e) {
+                fail("bytearray unexpectedly locked");
+            }
+        }
+        // Other types cannot be checked
+    }
+
+    /**
+     * Check that reusable PyBuffer is re-used, and that non-reusable PyBuffer is not re-used.
+     *
+     * @param subject
+     */
+    private void checkReusable(BufferProtocol subject, PyBuffer previous, PyBuffer latest) {
+        assertNotNull("Re-used PyBuffer reference null", latest);
+        if (subject instanceof PyByteArray) {
+            // Re-use prohibited because might have resized while released
+            assertFalse("PyByteArray buffer reused unexpectedly", latest == previous);
+        } else if (subject instanceof TestableExporter && !((TestableExporter)subject).reusable) {
+            // Special test case where re-use prohibited
+            assertFalse("PyBuffer reused unexpectedly", latest == previous);
+        } else {
+            // Other types of TestableExporter and PyString all re-use
+            assertTrue("PyBuffer not re-used as expected", latest == previous);
+        }
+    }
+
+    /**
+     * Test method for {@link org.python.core.PyBuffer#getBufferSlice(int, int, int, int)}.
+     */
+    public void testGetBufferSliceWithStride() {
+
+        for (BufferTestPair test : buffersToRead) {
+            if (verbosity > 0) {
+                System.out.println("getBufferSliceWithStride: " + test);
+            }
+            ByteMaterial material = test.material;
+            PyBuffer view = test.view;
+            boolean readonly = test.readonly;
+
+            // Generate some slices from the material and the test view
+            int N = material.length;
+            int M = (N + 4) / 4;    // At least one and about N/4
+
+            // For a range of start positions up to one beyond the end
+            for (int start = 0; start <= N; start += M) {
+                // For a range of lengths
+                for (int length : sliceLengths) {
+
+                    if (length == 0) {
+                        checkSlice(view, material, start, 0, 1, readonly);
+                        checkSlice(view, material, start, 0, 2, readonly);
+