[Jython-checkins] jython (merge default -> default): Merge of buffer API changes to trunk

jeff.allen jython-checkins at python.org
Sat Aug 27 09:12:26 EDT 2016


https://hg.python.org/jython/rev/c87db62a1b0e
changeset:   7950:c87db62a1b0e
parent:      7935:35fa19ca1859
parent:      7949:92b8a1f732f1
user:        Jeff Allen <ja.py at farowl.co.uk>
date:        Sat Aug 27 14:10:53 2016 +0100
summary:
  Merge of buffer API changes to trunk

files:
  NEWS                                                    |     6 +
  build.xml                                               |     4 +-
  src/org/python/core/PyArray.java                        |     9 +-
  src/org/python/core/PyBUF.java                          |    30 +-
  src/org/python/core/PyBuffer.java                       |   125 +-
  src/org/python/core/PyByteArray.java                    |     2 +-
  src/org/python/core/PyMemoryView.java                   |    17 +-
  src/org/python/core/PyString.java                       |     2 +-
  src/org/python/core/buffer/Base1DBuffer.java            |   117 +
  src/org/python/core/buffer/BaseArrayBuffer.java         |   228 +
  src/org/python/core/buffer/BaseBuffer.java              |   647 +-
  src/org/python/core/buffer/BaseNIOBuffer.java           |   249 +
  src/org/python/core/buffer/SimpleBuffer.java            |   180 +-
  src/org/python/core/buffer/SimpleNIOBuffer.java         |   190 +
  src/org/python/core/buffer/SimpleStringBuffer.java      |    87 +-
  src/org/python/core/buffer/SimpleWritableBuffer.java    |    94 +-
  src/org/python/core/buffer/Strided1DBuffer.java         |   157 +-
  src/org/python/core/buffer/Strided1DNIOBuffer.java      |   209 +
  src/org/python/core/buffer/Strided1DWritableBuffer.java |    82 +-
  src/org/python/core/buffer/ZeroByteBuffer.java          |    75 +-
  src/org/python/modules/_io/PyIOBase.java                |     9 +-
  tests/java/org/python/core/BaseBytesTest.java           |     7 +-
  tests/java/org/python/core/ByteBufferTestSupport.java   |   585 ++
  tests/java/org/python/core/PyBufferNIOTest.java         |   305 +
  tests/java/org/python/core/PyBufferTest.java            |  2419 ++++-----
  tests/java/org/python/core/PyBufferTestSupport.java     |   544 ++
  26 files changed, 4276 insertions(+), 2103 deletions(-)


diff --git a/NEWS b/NEWS
--- a/NEWS
+++ b/NEWS
@@ -22,6 +22,12 @@
     - Added uname function to posix module. The mostly Java-based implementation even
       works to some extend on non-posix systems. Additional tweaks extend this to full
       featured uname-functionality on Windows systems (usable via os.uname()).
+    - Buffer API changes allow java.nio.ByteBuffer to provide the storage when a PyBuffer
+      is exported. This is to support CPython extensions via JyNI, but has other uses too
+      (including access to direct memory buffers from Python). There is no change at the
+      Python level or for client code using PyBuffer via the "fully encapsulated" API. It
+      risks breaking code that makes direct access to a byte array via PyBuffer, implements
+      the PyBuffer interface, or extends implementation classes in org.python.core.buffer.
 
 Jython 2.7.1b3
   Bugs fixed
diff --git a/build.xml b/build.xml
--- a/build.xml
+++ b/build.xml
@@ -928,7 +928,7 @@
             <sysproperty key="python.cachedir.skip" value="true"/>
             <sysproperty key="python.home" value="${dist.dir}"/>
             <classpath refid="test.classpath"/>
-            <batchtest>
+            <batchtest skipNonTests="true">
                 <fileset dir="${test.source.dir}" includes="**/${test}.java"/>
             </batchtest>
         </junit>
@@ -950,7 +950,7 @@
             <sysproperty key="python.home" value="${dist.dir}"/>
             <sysproperty key="python.test.source.dir" value="${test.source.dir}"/>
             <classpath refid="test.classpath"/>
-            <batchtest todir="${junit.reports}">
+            <batchtest todir="${junit.reports}" skipNonTests="true">
                 <fileset dir="${test.source.dir}" includes="**/*Test*.java">
                     <exclude name="javatests/**/*" />
                     <exclude name="**/InterpTestCase.java" />
diff --git a/src/org/python/core/PyArray.java b/src/org/python/core/PyArray.java
--- a/src/org/python/core/PyArray.java
+++ b/src/org/python/core/PyArray.java
@@ -2081,10 +2081,10 @@
                 // This is byte data, so we are within the state of the art
                 byte[] storage = (byte[])data;
                 int size = delegate.getSize();
-                pybuf = new SimpleWritableBuffer(flags, storage, 0, size);
+                pybuf = new SimpleWritableBuffer(flags, this, storage, 0, size);
             } else if ((flags & PyBUF.WRITABLE) == 0) {
                 // As the client only intends to read, fake the answer with a String
-                pybuf = new SimpleStringBuffer(flags, tostring());
+                pybuf = new SimpleStringBuffer(flags, this, tostring());
             } else {
                 // For the time being ...
                 throw Py.NotImplementedError("only array('b') can export a writable buffer");
@@ -2163,7 +2163,6 @@
             return buf.remaining();
         }
 
-
         @Override
         public int read() {
             return buf.hasRemaining() ? buf.get() & 0xff : -1;
@@ -2187,7 +2186,6 @@
         }
     }
 
-
     /* Traverseproc implementation */
     @Override
     public int traverse(Visitproc visit, Object arg) {
@@ -2198,8 +2196,7 @@
     }
 
     @Override
-    public boolean refersDirectlyTo(PyObject ob)
-            throws UnsupportedOperationException {
+    public boolean refersDirectlyTo(PyObject ob) throws UnsupportedOperationException {
         if (data == null || !gc.canLinkToPyObject(data.getClass(), true)) {
             return false;
         }
diff --git a/src/org/python/core/PyBUF.java b/src/org/python/core/PyBUF.java
--- a/src/org/python/core/PyBUF.java
+++ b/src/org/python/core/PyBUF.java
@@ -51,17 +51,17 @@
     int[] getShape();
 
     /**
-     * The number of units (bytes) stored in each indexable item.
+     * The number of bytes stored in each indexable item.
      *
-     * @return the number of units (bytes) comprising each item.
+     * @return the number of bytes comprising each item.
      */
     int getItemsize();
 
     /**
-     * The total number of units (bytes) stored, which will be the product of the elements of the
-     * <code>shape</code> array, and the item size in units.
+     * The total number of bytes represented by the view, which will be the product of the elements of the
+     * <code>shape</code> array, and the item size in bytes.
      *
-     * @return the total number of units stored.
+     * @return the total number of bytes represented.
      */
     int getLen();
 
@@ -122,7 +122,7 @@
     /**
      * A constant used by the consumer in its call to {@link BufferProtocol#getBuffer(int)} to
      * specify that it requires {@link PyBuffer#getFormat()} to return a <code>String</code>
-     * indicating the type of the unit. This exists for compatibility with CPython, as Jython as the
+     * indicating the type of the unit. This exists for compatibility with CPython, as in Jython the
      * format is always provided by <code>getFormat()</code>.
      */
     static final int FORMAT = 0x0004;
@@ -143,7 +143,7 @@
     static final int STRIDES = 0x0010 | ND;
     /**
      * A constant used by the consumer in its call to {@link BufferProtocol#getBuffer(int)} to
-     * specify that it will assume C-order organisation of the units. <code>getBuffer</code> will
+     * specify that it will assume C-order organisation of the items. <code>getBuffer</code> will
      * raise an exception if the exporter's buffer is not C-ordered. <code>C_CONTIGUOUS</code>
      * implies <code>STRIDES</code>.
      */
@@ -152,14 +152,14 @@
     static final int C_CONTIGUOUS = 0x0020 | STRIDES;
     /**
      * A constant used by the consumer in its call to {@link BufferProtocol#getBuffer(int)} to
-     * specify that it will assume Fortran-order organisation of the units. <code>getBuffer</code>
+     * specify that it will assume Fortran-order organisation of the items. <code>getBuffer</code>
      * will raise an exception if the exporter's buffer is not Fortran-ordered.
      * <code>F_CONTIGUOUS</code> implies <code>STRIDES</code>.
      */
     static final int F_CONTIGUOUS = 0x0040 | STRIDES;
     /**
      * A constant used by the consumer in its call to {@link BufferProtocol#getBuffer(int)} to
-     * specify that it will assume a contiguous organisation of the units, but will enquire which
+     * specify that it will assume a contiguous organisation of the items, but will enquire which
      * organisation it actually is.
      *
      * <code>getBuffer</code> will raise an exception if the exporter's buffer is not contiguous.
@@ -203,13 +203,14 @@
      * 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()}.
+     * {@link PyBuffer#getNIOByteBuffer()} or using {@link PyBuffer#Pointer()}.
      */
     static final int FULL = INDIRECT | WRITABLE | FORMAT;
     /**
      * 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()}.
+     * only, without ever calling {@link PyBuffer#getNIOByteBuffer()} or using
+     * {@link PyBuffer#Pointer()}.
      */
     static final int FULL_RO = INDIRECT | FORMAT;
 
@@ -221,7 +222,8 @@
      * through the purely abstract part of the API). <code>getBuffer</code> will raise an exception
      * if the exporter cannot expose its storage as Java array.
      */
-    static final int AS_ARRAY = 0x10000000;
+    // XXX Pending: @Deprecated
+     static final int AS_ARRAY = 0x10000000;
 
     /* Constants for readability, not standard for CPython */
 
@@ -242,13 +244,13 @@
     static final int NAVIGATION = SIMPLE | ND | STRIDES | INDIRECT;
     /**
      * A constant used by the exporter in processing {@link BufferProtocol#getBuffer(int)} to check
-     * for assumed C-order organisation of the units.
+     * for assumed C-order organisation of the items.
      * <code>C_CONTIGUOUS = IS_C_CONTIGUOUS | STRIDES</code>.
      */
     static final int IS_C_CONTIGUOUS = C_CONTIGUOUS & ~STRIDES;
     /**
      * A constant used by the exporter in processing {@link BufferProtocol#getBuffer(int)} to check
-     * for assumed C-order Fortran-order organisation of the units.
+     * for assumed C-order Fortran-order organisation of the items.
      * <code>F_CONTIGUOUS = IS_F_CONTIGUOUS | STRIDES</code>.
      */
     static final int IS_F_CONTIGUOUS = F_CONTIGUOUS & ~STRIDES;
diff --git a/src/org/python/core/PyBuffer.java b/src/org/python/core/PyBuffer.java
--- a/src/org/python/core/PyBuffer.java
+++ b/src/org/python/core/PyBuffer.java
@@ -30,6 +30,14 @@
     // int getLen();
 
     /**
+     * Return the underlying exporting object (or <code>null</code> if no object implementing the
+     * {@link BufferProtocol} is in that role). This will often be a <code>PyObject</code>.
+     *
+     * @return exporting object (or <code>null</code>)
+     */
+    BufferProtocol getObj();
+
+    /**
      * Return the byte indexed from a one-dimensional buffer with item size one. This is part of the
      * fully-encapsulated API: the buffer implementation exported takes care of navigating the
      * structure of the buffer. Results are undefined where the number of dimensions is not one or
@@ -106,47 +114,46 @@
      * further study.)
      *
      * @param dest destination byte array
-     * @param destPos index in the destination array of the byte [0]
+     * @param destPos byte-index in the destination array of the byte [0]
      * @throws IndexOutOfBoundsException if the destination cannot hold it
      */
     void copyTo(byte[] dest, int destPos) throws IndexOutOfBoundsException, PyException;
 
     /**
-     * Copy a simple slice of the buffer to the destination byte array, defined by a starting index
-     * and length in the source buffer. This may validly be done only for a one-dimensional buffer,
-     * as the meaning of the starting index is otherwise not defined. The length (like the source
-     * index) is in source buffer <b>items</b>: <code>length*itemsize</code> bytes will be occupied
-     * in the destination.
+     * Copy a simple slice of the buffer-view to the destination byte array, defined by a starting
+     * item-index in the source buffer and the <code>count</code> of items to copy. This may validly
+     * be done only for a one-dimensional buffer, as the meaning of the starting item-index is
+     * otherwise not defined. <code>count*itemsize</code> bytes will be occupied in the destination.
      *
-     * @param srcIndex starting index in the source buffer
+     * @param srcIndex starting item-index in the source buffer
      * @param dest destination byte array
-     * @param destPos index in the destination array of the item [0,...]
-     * @param length number of items to copy
+     * @param destPos byte-index in the destination array of the source item [0,...]
+     * @param count number of items to copy
      * @throws IndexOutOfBoundsException if access out of bounds in source or destination
      */
-    void copyTo(int srcIndex, byte[] dest, int destPos, int length)     // mimic arraycopy args
+    void copyTo(int srcIndex, byte[] dest, int destPos, int count)     // mimic arraycopy args
             throws IndexOutOfBoundsException, PyException;
 
     /**
-     * Copy bytes from a slice of a (Java) byte array into the buffer. This may validly be done only
-     * for a one-dimensional buffer, as the meaning of the starting index is not otherwise defined.
-     * The length (like the destination index) is in buffer <b>items</b>:
-     * <code>length*itemsize</code> bytes will be read from the source.
+     * Copy from a slice of a (Java) byte array into the buffer starting at a given destination
+     * item-index. This may validly be done only for a one-dimensional buffer, as the meaning of the
+     * destination index is not otherwise defined. <code>count*itemsize</code> bytes will be read
+     * from the source.
      *
      * @param src source byte array
      * @param srcPos location in source of first byte to copy
-     * @param destIndex starting index in the destination (i.e. <code>this</code>)
-     * @param length number of bytes to copy in
+     * @param destIndex starting item-index in the destination (i.e. <code>this</code>)
+     * @param count number of items to copy in
      * @throws IndexOutOfBoundsException if access out of bounds in source or destination
      * @throws PyException (TypeError) if read-only buffer
      */
-    void copyFrom(byte[] src, int srcPos, int destIndex, int length)    // mimic arraycopy args
+    void copyFrom(byte[] src, int srcPos, int destIndex, int count)    // mimic arraycopy args
             throws IndexOutOfBoundsException, PyException;
 
     /**
-     * Copy the whole of another PyBuffer into this buffer. This may validly be done only for
-     * buffers that are consistent in their dimensions. When it is necessary to copy partial
-     * buffers, this may be achieved using a buffer slice on the source or destination.
+     * Copy the whole of another <code>PyBuffer</code> into this buffer. This may validly be done
+     * only for buffers that are consistent in their dimensions. When it is necessary to copy
+     * partial buffers, this may be achieved using a buffer slice on the source or destination.
      *
      * @param src source buffer
      * @throws IndexOutOfBoundsException if access out of bounds in source or destination
@@ -202,10 +209,10 @@
      *
      * @param flags specifying features demanded and the navigational capabilities of the consumer
      * @param start index in the current buffer
-     * @param length number of items in the required slice
+     * @param count number of items in the required slice
      * @return a buffer representing the slice
      */
-    public PyBuffer getBufferSlice(int flags, int start, int length);
+    public PyBuffer getBufferSlice(int flags, int start, int count);
 
     /**
      * Get a <code>PyBuffer</code> that represents a slice of the current one described in terms of
@@ -217,7 +224,7 @@
      * Suppose that <i>x(i)</i> denotes the <i>i</i>th element of the current buffer, that is, the
      * byte retrieved by <code>this.byteAt(i)</code> or the unit indicated by
      * <code>this.getPointer(i)</code>. A request for a slice where <code>start</code> <i>= s</i>,
-     * <code>length</code> <i>= N</i> and <code>stride</code> <i>= m</i>, results in a buffer
+     * <code>count</code> <i>= N</i> and <code>stride</code> <i>= m</i>, results in a buffer
      * <i>y</i> such that <i>y(k) = x(s+km)</i> where <i>k=0..(N-1)</i>. In Python terms, this is
      * the slice <i>x[s : s+(N-1)m+1 : m]</i> (if <i>m>0</i>) or the slice <i>x[s : s+(N-1)m-1 :
      * m]</i> (if <i>m<0</i>). Implementations should check that this range is entirely within
@@ -226,26 +233,60 @@
      * In a simple buffer backed by a contiguous byte array, the result is a strided PyBuffer on the
      * same storage but where the offset is adjusted by <i>s</i> and the stride is as supplied. If
      * the current buffer is already strided and/or has an item size larger than single bytes, the
-     * new <code>start</code> index, <code>length</code> and <code>stride</code> will be translated
+     * new <code>start</code> index, <code>count</code> and <code>stride</code> will be translated
      * from the arguments given, through this buffer's stride and item size. The caller always
      * expresses <code>start</code> and <code>strides</code> in terms of the abstract view of this
      * buffer.
      *
      * @param flags specifying features demanded and the navigational capabilities of the consumer
      * @param start index in the current buffer
-     * @param length number of items in the required slice
+     * @param count number of items in the required slice
      * @param stride index-distance in the current buffer between consecutive items in the slice
      * @return a buffer representing the slice
      */
-    public PyBuffer getBufferSlice(int flags, int start, int length, int stride);
+    public PyBuffer getBufferSlice(int flags, int start, int count, int stride);
 
-    // java.nio access to actual storage
+    // Access to underlying byte-oriented storage
     //
 
     /**
+     * Convert an item index (for a one-dimensional buffer) to an absolute byte index in the storage
+     * shared by the exporter. The storage exported as a <code>PyBuffer</code> is a linearly-indexed
+     * sequence of bytes, although it may not actually be a heap-allocated Java <code>byte[]</code>
+     * object. The purpose of this method is to allow the exporter to define the relationship
+     * between the item index (as used in {@link #byteAt(int)}) and the byte-index (as used with the
+     * <code>ByteBuffer</code> returned by {@link #getNIOByteBuffer()}). See
+     * {@link #byteIndex(int[])} for discussion of the multi-dimensional case.
+     *
+     * @param index item-index from consumer
+     * @return corresponding byte-index in actual storage
+     */
+    // Should it throw IndexOutOfBoundsException if the index <0 or ≥<code>shape[0]</code?
+    int byteIndex(int index) throws IndexOutOfBoundsException;
+
+    /**
+     * Convert a multi-dimensional item index to an absolute byte index in the storage shared by the
+     * exporter. The storage exported as a <code>PyBuffer</code> is a linearly-indexed sequence of
+     * bytes, although it may not actually be a heap-allocated Java <code>byte[]</code> object. The
+     * purpose of this method is to allow the exporter to define the relationship between the item
+     * index (as used in {@link #byteAt(int...)} and the byte-index (as used with the
+     * <code>ByteBuffer</code> returned by {@link #getNIOByteBuffer()}).
+     *
+     * @param indices n-dimensional item-index from consumer
+     * @return corresponding byte-index in actual storage
+     */
+    // Should it throw IndexOutOfBoundsException if any index <0 or ≥<code>shape[i]</code>?
+    int byteIndex(int... indices);
+
+    /**
      * Obtain a {@link java.nio.ByteBuffer} giving access to the bytes that hold 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>:
+     * exported by the original object. The position of the buffer is at the first byte of the item
+     * with zero index (quite possibly not the lowest valid byte-index), the limit of the buffer is
+     * beyond the largest valid byte index, and the mark is undefined.
+     * <p>
+     * For a one-dimensional contiguous buffer, the limit is one byte beyond the last item, so that
+     * consecutive reads from the <code>ByteBuffer</code> return the data in order. Assuming the
+     * following client code where <code>obj</code> has type <code>BufferProtocol</code>:
      *
      * <pre>
      * PyBuffer a = obj.getBuffer(PyBUF.SIMPLE);
@@ -253,34 +294,31 @@
      * ByteBuffer bb = a.getNIOBuffer();
      * </pre>
      *
-     * the item with index <code>bb.pos()+k</code> is in the buffer <code>bb</code> at positions
+     * the item with index <code>k</code> is in <code>bb</code> at positions
      * <code>bb.pos()+k*itemsize</code> to <code>bb.pos()+(k+1)*itemsize - 1</code> inclusive. And
      * if <code>itemsize==1</code>, the item is simply the byte at position <code>bb.pos()+k</code>.
-     * The buffer limit is set to the first byte beyond the valid data. A block read or write will
-     * therefore access the contents sequentially.
      * <p>
      * If the buffer is multidimensional or non-contiguous (strided), the buffer position is still
-     * the (first byte of) the item at index <code>[0]</code> or <code>[0,...,0]</code>, and the
-     * limit is one item beyond the valid data. However, it is necessary to navigate <code>bb</code>
-     * using the <code>shape</code>, <code>strides</code> and maybe <code>suboffsets</code> provided
-     * by the API.
+     * the (first byte of) the item at index <code>[0]</code> or <code>[0,...,0]</code>. However, it
+     * is necessary to navigate <code>bb</code> using the <code>shape</code>, <code>strides</code>
+     * and maybe <code>suboffsets</code> provided by the API.
      *
-     * @return a ByteBuffer equivalent to the exported data contents.
+     * @return a <code>ByteBuffer</code> onto the exported data contents.
      */
     ByteBuffer getNIOByteBuffer();
 
-    // Direct access to actual storage
-    //
-
     /**
-     * Determine whether the exporter is able to offer direct access to the exported storage as a
-     * Java byte array (through the API that involves class {@link Pointer}), or only supports the
+     * Report whether the exporter is able to offer direct access to the exported storage as a Java
+     * byte array (through the API that involves class {@link Pointer}), or only supports the
      * abstract API. See also {@link PyBUF#AS_ARRAY}.
      *
      * @return true if array access is supported, false if it is not.
      */
     boolean hasArray();
 
+    // Direct access to actual storage (deprecated)
+    //
+
     /**
      * A class that references a <code>byte[]</code> array and a particular offset within it, as the
      * return type for methods that give direct access to byte-oriented data exported by a Python
@@ -288,6 +326,7 @@
      * this array, and in others not. See {@link PyBuffer#isReadonly()}. It is used by the Jython
      * buffer API roughly where the CPython buffer API uses a C (char *) pointer.
      */
+    @Deprecated
     public static class Pointer {
 
         /** Reference to the array holding the bytes. */
@@ -379,7 +418,7 @@
      * free to navigate the underlying buffer <code>b.storage</code> without respecting these
      * boundaries. If the buffer is non-contiguous, the above description is still valid (since a
      * multi-byte item must itself be contiguously stored), but in any additional navigation of
-     * <code>b.storage[]</code> to other units, the client must use the shape, strides and
+     * <code>b.storage[]</code> to other items, the client must use the shape, strides and
      * sub-offsets provided by the API. Normally one starts <code>b = a.getBuf()</code> in order to
      * establish the offset of index [0,...,0].
      *
diff --git a/src/org/python/core/PyByteArray.java b/src/org/python/core/PyByteArray.java
--- a/src/org/python/core/PyByteArray.java
+++ b/src/org/python/core/PyByteArray.java
@@ -228,7 +228,7 @@
 
         if (pybuf == null) {
             // No existing export we can re-use: create a new one
-            pybuf = new SimpleWritableBuffer(flags, storage, offset, size);
+            pybuf = new SimpleWritableBuffer(flags, this, storage, offset, size);
             // Hold a reference for possible re-use
             export = new WeakReference<BaseBuffer>(pybuf);
         }
diff --git a/src/org/python/core/PyMemoryView.java b/src/org/python/core/PyMemoryView.java
--- a/src/org/python/core/PyMemoryView.java
+++ b/src/org/python/core/PyMemoryView.java
@@ -75,6 +75,13 @@
         }
     }
 
+    // @ExposedGet(doc = obj_doc) // Not exposed in Python 2.7
+    public PyObject obj() {
+        checkNotReleased();
+        BufferProtocol obj = backing.getObj();
+        return (obj instanceof PyObject) ? (PyObject)obj : Py.None;
+    }
+
     @ExposedGet(doc = format_doc)
     public String format() {
         checkNotReleased();
@@ -843,19 +850,18 @@
         }
     }
 
-
     /* Traverseproc implementation */
     @Override
     public int traverse(Visitproc visit, Object arg) {
         int retVal;
         if (backing != null) {
             if (backing instanceof PyObject) {
-                retVal = visit.visit((PyObject) backing, arg);
+                retVal = visit.visit((PyObject)backing, arg);
                 if (retVal != 0) {
                     return retVal;
                 }
             } else if (backing instanceof Traverseproc) {
-                retVal = ((Traverseproc) backing).traverse(visit, arg);
+                retVal = ((Traverseproc)backing).traverse(visit, arg);
                 if (retVal != 0) {
                     return retVal;
                 }
@@ -878,11 +884,10 @@
 
     @Override
     public boolean refersDirectlyTo(PyObject ob) {
-        if (ob != null && (ob == backing || ob == shape || ob == strides
-            || ob == suboffsets)) {
+        if (ob != null && (ob == backing || ob == shape || ob == strides || ob == suboffsets)) {
             return true;
         } else if (suboffsets instanceof Traverseproc) {
-            return ((Traverseproc) suboffsets).refersDirectlyTo(ob);
+            return ((Traverseproc)suboffsets).refersDirectlyTo(ob);
         } else {
             return false;
         }
diff --git a/src/org/python/core/PyString.java b/src/org/python/core/PyString.java
--- a/src/org/python/core/PyString.java
+++ b/src/org/python/core/PyString.java
@@ -204,7 +204,7 @@
              * No existing export we can re-use. Return a buffer, but specialised to defer
              * construction of the buf object, and cache a soft reference to it.
              */
-            pybuf = new SimpleStringBuffer(flags, getString());
+            pybuf = new SimpleStringBuffer(flags, this, getString());
             export = new SoftReference<BaseBuffer>(pybuf);
         }
         return pybuf;
diff --git a/src/org/python/core/buffer/Base1DBuffer.java b/src/org/python/core/buffer/Base1DBuffer.java
new file mode 100644
--- /dev/null
+++ b/src/org/python/core/buffer/Base1DBuffer.java
@@ -0,0 +1,117 @@
+package org.python.core.buffer;
+
+import org.python.core.PyBUF;
+
+/**
+ * Base implementation of the Buffer API appropriate to 1-dimensional arrays, of any item size,
+ * independent of the storage implementation. The description of {@link BaseBuffer} mostly applies.
+ */
+public abstract class Base1DBuffer extends BaseBuffer {
+
+    /** The strides array for a contiguous 1D byte buffer. */
+    protected static final int[] ONE = {1};
+
+    /** The shape array for a zero length 1D buffer. */
+    protected static final int[] ZERO = {0};
+
+    /**
+     * Construct an instance of <code>Base1DBuffer</code> in support of a sub-class, specifying the
+     * 'feature flags', or at least a starting set to be adjusted later. Also specify the navigation
+     * ( {@link #index0}, number of elements, and {@link #strides} array. These 'feature flags' are
+     * the features of the buffer exported, not the flags that form the consumer's request. The
+     * buffer will be read-only unless {@link PyBUF#WRITABLE} is set. {@link PyBUF#FORMAT} is
+     * implicitly added to the feature flags.
+     * <p>
+     * To complete initialisation, the sub-class normally must create its own wrapped byte-storage,
+     * and call {@link #checkRequestFlags(int)} passing the consumer's request flags.
+     *
+     * @param featureFlags bit pattern that specifies the features allowed
+     * @param index0 index into storage of <code>item[0]</code>
+     * @param size number of elements in the view
+     * @param strides an array of length 1 providing index stride between successive elements
+     */
+    protected Base1DBuffer(int featureFlags, int index0, int size, int[] strides) {
+        super(featureFlags, index0, size == 0 ? ZERO : new int[] {size}, strides);
+    }
+
+    /**
+     * Construct an instance of <code>Base1DBuffer</code> in support of a sub-class, specifying the
+     * 'feature flags', or at least a starting set to be adjusted later. Also specify the navigation
+     * ( {@link #index0}, number of elements, and byte-index distance from one to the next. These
+     * 'feature flags' are the features of the buffer exported, not the flags that form the
+     * consumer's request. The buffer will be read-only unless {@link PyBUF#WRITABLE} is set.
+     * {@link PyBUF#FORMAT} is implicitly added to the feature flags.
+     * <p>
+     * To complete initialisation, the sub-class normally must create its own wrapped byte-storage,
+     * and call {@link #checkRequestFlags(int)} passing the consumer's request flags.
+     *
+     * @param featureFlags bit pattern that specifies the features allowed
+     * @param index0 index into storage of <code>item[0]</code>
+     * @param size number of elements in the view
+     * @param stride byte-index distance from one element to the next
+     */
+    protected Base1DBuffer(int featureFlags, int index0, int size, int stride) {
+        this(featureFlags, index0, size, stride == 1 ? ONE : new int[] {stride});
+    }
+
+    @Override
+    protected int getSize() {
+        return shape[0];
+    }
+
+    @Override
+    public int getLen() {
+        return shape[0] * getItemsize();
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Specialised to one-dimensional, possibly strided buffer.
+     */
+    @Override
+    protected int calcGreatestIndex() {
+        int stride = strides[0];
+        if (stride == 1) {
+            return index0 + shape[0] - 1;
+        } else if (stride > 0) {
+            return index0 + (shape[0] - 1) * stride;
+        } else {
+            return index0;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Specialised to one-dimensional, possibly strided buffer.
+     */
+    @Override
+    protected int calcLeastIndex() {
+        int stride = strides[0];
+        if (stride < 0) {
+            return index0 + (shape[0] - 1) * stride;
+        } else {
+            return index0;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Specialised in <code>BaseArrayBuffer</code> to one dimension.
+     */
+    @Override
+    public boolean isContiguous(char order) {
+        if ("CFA".indexOf(order) < 0) {
+            return false;
+        } else {
+            if (getShape()[0] < 2) {
+                return true;
+            } else {
+                return getStrides()[0] == getItemsize();
+            }
+        }
+    }
+
+}
diff --git a/src/org/python/core/buffer/BaseArrayBuffer.java b/src/org/python/core/buffer/BaseArrayBuffer.java
new file mode 100644
--- /dev/null
+++ b/src/org/python/core/buffer/BaseArrayBuffer.java
@@ -0,0 +1,228 @@
+package org.python.core.buffer;
+
+import java.nio.ByteBuffer;
+
+import org.python.core.PyBUF;
+import org.python.core.PyBuffer;
+import org.python.core.PyException;
+
+/**
+ * Base implementation of the Buffer API for when the storage implementation is <code>byte[]</code>.
+ * The description of {@link BaseBuffer} mostly applies. Methods provided or overridden here are
+ * appropriate to 1-dimensional arrays, of any item size, backed by <code>byte[]</code>.
+ */
+public abstract class BaseArrayBuffer extends Base1DBuffer {
+
+    /**
+     * Reference to the underlying <code>byte[]</code> storage that the exporter is sharing with the
+     * consumer. The data need not occupy the whole array: in the constructor of a particular type
+     * of buffer, the exporter usually indicates an offset to the first significant byte and length
+     * (contiguous cases) or the index in <code>storage</code> that should be treated as the item
+     * with index zero (retrieved say by <code>buf.byteAt(0)</code>).
+     */
+    protected byte[] storage;
+
+    /**
+     * Construct an instance of <code>BaseArrayBuffer</code> in support of a sub-class, specifying
+     * the 'feature flags', or at least a starting set to be adjusted later. Also specify the
+     * navigation ( {@link #index0}, number of elements, and stride. These 'feature flags' are the
+     * features of the buffer exported, not the flags that form the consumer's request. The buffer
+     * will be read-only unless {@link PyBUF#WRITABLE} is set. {@link PyBUF#FORMAT} and
+     * {@link PyBUF#AS_ARRAY} are implicitly added to the feature flags.
+     * <p>
+     * To complete initialisation, the sub-class normally must call {@link #checkRequestFlags(int)}
+     * passing the consumer's request flags.
+     *
+     * @param storage the array of bytes storing the implementation of the exporting object
+     * @param featureFlags bit pattern that specifies the features allowed
+     * @param index0 index into storage of <code>item[0]</code>
+     * @param size number of elements in the view
+     * @param stride byte-index distance from one element to the next
+     */
+    protected BaseArrayBuffer(byte[] storage, int featureFlags, int index0, int size, int stride) {
+        super(featureFlags | AS_ARRAY, index0, size, stride);
+        this.storage = storage;
+    }
+
+    @Override
+    protected byte byteAtImpl(int byteIndex) throws IndexOutOfBoundsException {
+        return storage[byteIndex];
+    }
+
+    @Override
+    protected void storeAtImpl(byte value, int byteIndex) throws IndexOutOfBoundsException,
+            PyException {
+        checkWritable();
+        storage[byteIndex] = value;
+    }
+
+    @Override
+    public int byteIndex(int... indices) throws IndexOutOfBoundsException {
+        // BaseBuffer implementation can be simplified since if indices.length!=1 we error.
+        checkDimension(indices.length); // throws if != 1
+        return byteIndex(indices[0]);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The implementation in <code>BaseArrayBuffer</code> deals with the general one-dimensional
+     * case of arbitrary item size and stride.
+     */
+    @Override
+    public void copyTo(int srcIndex, byte[] dest, int destPos, int count)
+            throws IndexOutOfBoundsException {
+
+        if (count > 0) {
+            // Pick up attributes necessary to choose an efficient copy strategy
+            int itemsize = getItemsize();
+            int stride = getStrides()[0];
+            int skip = stride - itemsize;
+            int s = byteIndex(srcIndex);
+
+            // Strategy depends on whether items are laid end-to-end contiguously or there are gaps
+            if (skip == 0) {
+                // stride == itemsize: straight copy of contiguous bytes
+                System.arraycopy(storage, s, dest, destPos, count * itemsize);
+            } else {
+                int limit = s + count * stride, d = destPos;
+                if (itemsize == 1) {
+                    // Non-contiguous copy: single byte items
+                    for (; s != limit; s += stride) {
+                        dest[d++] = storage[s];
+                    }
+                } else {
+                    // Non-contiguous copy: each time, copy itemsize bytes then skip
+                    for (; s != limit; s += skip) {
+                        int t = s + itemsize;
+                        while (s < t) {
+                            dest[d++] = storage[s++];
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation in <code>BaseArrayBuffer</code> deals with the general
+     * one-dimensional case of arbitrary item size and stride.
+     */
+    @Override
+    public void copyFrom(byte[] src, int srcPos, int destIndex, int count)
+            throws IndexOutOfBoundsException, PyException {
+        copyFrom(src, srcPos, 1, destIndex, count);
+    }
+
+    /**
+     * Generalisation of {@link PyBuffer#copyFrom(byte[], int, int, int)} to allow a stride within
+     * the source array.
+     *
+     * @param src source byte array
+     * @param srcPos byte-index location in source of first byte to copy
+     * @param srcStride byte-index increment from one item to the next
+     * @param destIndex starting item-index in the destination (i.e. <code>this</code>)
+     * @param count number of items to copy in
+     * @throws IndexOutOfBoundsException if access out of bounds in source or destination
+     * @throws PyException (TypeError) if read-only buffer
+     */
+    protected void copyFrom(byte[] src, int srcPos, int srcStride, int destIndex, int count)
+            throws IndexOutOfBoundsException, PyException {
+
+        checkWritable();
+
+        if (count > 0) {
+            // Pick up attributes necessary to choose an efficient copy strategy
+            int itemsize = getItemsize();
+            int stride = getStrides()[0];
+            int skip = stride - itemsize;
+            int d = byteIndex(destIndex);
+
+            int srcSkip = srcStride - itemsize;
+
+            // Strategy depends on whether items are laid end-to-end or there are gaps
+            if (skip == 0 && srcSkip == 0) {
+                // Straight copy of contiguous bytes
+                System.arraycopy(src, srcPos, storage, d, count * itemsize);
+            } else {
+                int limit = d + count * stride, s = srcPos;
+                if (itemsize == 1) {
+                    // Non-contiguous copy: single byte items
+                    for (; d != limit; d += stride) {
+                        storage[d] = src[s];
+                        s += srcStride;
+                    }
+                } else {
+                    // Non-contiguous copy: itemsize bytes then skip to next item
+                    for (; d != limit; d += skip) {
+                        int t = d + itemsize;
+                        while (d < t) {
+                            storage[d++] = src[s++];
+                        }
+                        s += srcSkip;
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException {
+        if (src instanceof BaseArrayBuffer && !this.overlaps((BaseArrayBuffer)src)) {
+            // We can do this efficiently, copying between arrays.
+            copyFromArrayBuffer((BaseArrayBuffer)src);
+        } else {
+            super.copyFrom(src);
+        }
+    }
+
+    private boolean overlaps(BaseArrayBuffer src) {
+        if (src.storage != this.storage) {
+            return false;
+        } else {
+            int low = calcLeastIndex(), high = calcGreatestIndex();
+            int srcLow = src.calcLeastIndex(), srcHigh = src.calcGreatestIndex();
+            return (srcHigh >= low && high >= srcLow);
+        }
+    }
+
+    private void copyFromArrayBuffer(BaseArrayBuffer src) throws IndexOutOfBoundsException,
+            PyException {
+
+        src.checkDimension(1);
+
+        int itemsize = getItemsize();
+        int count = getSize();
+
+        // Block operation if different item or overall size
+        if (src.getItemsize() != itemsize || src.getSize() != count) {
+            throw differentStructure();
+        }
+
+        // We depend on the striding copyFrom() acting directly on the source storage
+        copyFrom(src.storage, src.index0, src.strides[0], 0, count);
+    }
+
+    @Override
+    protected ByteBuffer getNIOByteBufferImpl() {
+        // The buffer spans the whole storage, which may include data not in the view
+        ByteBuffer b = ByteBuffer.wrap(storage);
+        // Return as read-only if it is.
+        return isReadonly() ? b.asReadOnlyBuffer() : b;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * <code>BaseArrayBuffer</code> provides a reference to the storage array even when the buffer
+     * is intended not to be writable. There can be no enforcement of read-only character once a
+     * reference to the byte array has been handed out.
+     */
+    @SuppressWarnings("deprecation")
+    @Override
+    public Pointer getBuf() {
+        return new Pointer(storage, index0);
+    }
+}
diff --git a/src/org/python/core/buffer/BaseBuffer.java b/src/org/python/core/buffer/BaseBuffer.java
--- a/src/org/python/core/buffer/BaseBuffer.java
+++ b/src/org/python/core/buffer/BaseBuffer.java
@@ -9,34 +9,35 @@
 import org.python.core.PyException;
 
 /**
- * Base implementation of the Buffer API providing variables and accessors for the navigational
+ * Base implementation of the Buffer API providing variables and accessors for the navigation
  * 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,
- * non-contiguous storage and items that are not single bytes must override the default
- * implementations.
+ * This class leaves undefined the storage mechanism for the bytes (typically <code>byte[]</code> or
+ * <code>java.nio.ByteBuffer</code>), while remaining definite that it is an indexable sequence of
+ * bytes. A concrete class that extends this one must provide elementary accessors
+ * {@link #byteAtImpl(int)}, {@link #storeAtImpl(byte, int)} that abstract this storage, a factory
+ * {@link #getNIOByteBufferImpl()} for <code>ByteBuffer</code>s that wrap the storage, and a factory
+ * for slices {@link #getBufferSlice(int, int, int, int)}.
+ * <p>
+ * The sub-class constructor must specify the feature flags (see {@link #BaseBuffer(int)}), set
+ * {@link #index0}, {@link #shape} and {@link #strides}, and finally check the client capabilities
+ * with {@link #checkRequestFlags(int)}. Sub-classes intended to represent slices of exporters that
+ * must count their exports as part of a locking protocol, as does <code>bytearray</code>, must
+ * override {@link #getRoot()} so that a buffer view {@link #release()} on a slice, propagates to
+ * the buffer view that provided it.
+ * <p>
+ * Access methods provided here necessarily work with the abstracted {@link #byteAtImpl(int)},
+ * {@link #storeAtImpl(byte, int)} interface, but subclasses are able to override them with more
+ * efficient versions that employ knowledge of the particular storage type used.
  * <p>
  * This base implementation is writable only if {@link PyBUF#WRITABLE} is in the feature flags
- * passed to the constructor. Otherwise, all methods for write access raise a
- * <code>BufferError</code> read-only exception and {@link #isReadonly()} returns <code>true</code>.
- * Sub-classes can follow the same pattern, setting {@link PyBUF#WRITABLE} in the constructor and,
- * if they have to, overriding the operations that write (<code>storeAt</code> and
- * <code>copyFrom</code>). The recommended pattern is:
- *
- * <pre>
- * if (isReadonly()) {
- *     throw notWritable();
- * }
- * // ... implementation of the write operation
- * </pre>
- * Another approach, used in the standard library, is to have distinct classes for the writable and
- * read-only variants. The implementors of simple buffers will find it efficient to override the
- * generic access methods to which performance might be sensitive, with a calculation specific to
- * their actual type.
+ * passed to the constructor. Otherwise, all methods for write access raise a <code>TypeError</code>
+ * and {@link #isReadonly()} returns <code>true</code>. However, a client intending to write should
+ * have presented {@link PyBUF#WRITABLE} in its client request flags when getting the buffer, and
+ * been prevented by a <code>BufferError</code> exception at that point.
  * <p>
  * At the time of writing, only one-dimensional buffers of item size one are used in the Jython
  * core.
@@ -44,6 +45,12 @@
 public abstract class BaseBuffer implements PyBuffer {
 
     /**
+     * The object that exported this buffer (or <code>null</code> if the subclass or exporter
+     * chooses not to supply a reference).
+     */
+    protected BufferProtocol obj;
+
+    /**
      * The dimensions of the array represented by the buffer. The length of the <code>shape</code>
      * array is the number of dimensions. The <code>shape</code> array should always be created and
      * filled (difference from CPython). This value is returned by {@link #getShape()}.
@@ -59,19 +66,11 @@
     protected int[] strides;
 
     /**
-     * Reference to the underlying <code>byte[]</code> storage that the exporter is sharing with the
-     * consumer. The data need not occupy the whole array: in the constructor of a particular type
-     * of buffer, the exporter usually indicates an offset to the first significant byte and length
-     * (contiguous cases) or the index in <code>storage</code> that should be treated as the item
-     * with index zero (retrieved say by {@link #byteAt(int)}).
-     */
-    protected byte[] storage;
-
-    /**
-     * Absolute index in <code>storage</code> of <code>item[0]</code>. In one dimension, for a
-     * positive <code>stride</code> this is equal to the offset of the first byte used in
-     * {@link #storage}, and for a negative <code>stride</code> it is the last. In an N-dimensional
-     * buffer with strides of mixed sign, it could be anywhere in the data.
+     * Absolute byte-index in the storage of <code>item[0]</code>. In one dimension, for a positive
+     * <code>stride</code> this is equal to the offset of the first byte used in whatever
+     * byte-storage is provided, and for a negative <code>stride</code> it is the first byte of the
+     * last item. In an N-dimensional buffer with strides of mixed sign, it could be anywhere in the
+     * data.
      */
     protected int index0;
 
@@ -91,8 +90,8 @@
      * the consumer does not specify that it will use a navigation array the buffer requires.
      * <p>
      * In order to support efficient checking with {@link #checkRequestFlags(int)} we store a
-     * mutilated version of the apparent <code>featureFlags</code> in which the non-navigational
-     * flags are inverted. The syndrome <code>S</code> of the error is computed as follows. Let
+     * mutilated version of the apparent <code>featureFlags</code> in which the non-navigation flags
+     * are inverted. The syndrome <code>S</code> of the error is computed as follows. Let
      * <code>N=1</code> where we are dealing with a navigation flag, let <code>F</code> be a buffer
      * feature flag, and let <code>X</code> be the consumer request flags.
      *
@@ -125,20 +124,26 @@
     private int gFeatureFlags = ~NAVIGATION; // featureFlags = 0
 
     /**
-     * Construct an instance of BaseBuffer in support of a sub-class, specifying the 'feature
-     * flags', or at least a starting set to be adjusted later. These are the features of the buffer
-     * exported, not the flags that form the consumer's request. The buffer will be read-only unless
-     * {@link PyBUF#WRITABLE} is set in the feature flags. {@link PyBUF#FORMAT} and
-     * {@link PyBUF#AS_ARRAY} are implicitly added to the feature flags. The navigation arrays are
-     * all null, awaiting action by the sub-class constructor. To complete initialisation, the
-     * sub-class normally must assign: the buffer ( {@link #storage}, {@link #index0}), and the
-     * navigation arrays ({@link #shape}, {@link #strides}), and call
-     * {@link #checkRequestFlags(int)} passing the consumer's request flags.
+     * Construct an instance of <code>BaseBuffer</code> in support of a sub-class, specifying the
+     * 'feature flags', or at least a starting set to be adjusted later. Also specify the navigation
+     * ( {@link #index0}, {@link #shape}, and {@link #strides}). These 'feature flags' are the
+     * features of the buffer exported, not the flags that form the consumer's request. The buffer
+     * will be read-only unless {@link PyBUF#WRITABLE} is set. {@link PyBUF#FORMAT} is implicitly
+     * added to the feature flags.
+     * <p>
+     * To complete initialisation, the sub-class normally must create its own wrapped byte-storage,
+     * and call {@link #checkRequestFlags(int)} passing the consumer's request flags.
      *
-     * @param featureFlags bit pattern that specifies the actual features allowed/required
+     * @param featureFlags bit pattern that specifies the features allowed
+     * @param index0 index into storage of <code>item[0,...,0]</code>
+     * @param shape elements in each dimension
+     * @param strides between successive elements in each dimension
      */
-    protected BaseBuffer(int featureFlags) {
-        setFeatureFlags(featureFlags | FORMAT | AS_ARRAY);
+    protected BaseBuffer(int featureFlags, int index0, int[] shape, int[] strides) {
+        setFeatureFlags(featureFlags | FORMAT);
+        this.index0 = index0;
+        this.shape = shape;
+        this.strides = strides;
     }
 
     /**
@@ -175,6 +180,17 @@
     }
 
     /**
+     * Remove features from this buffer expressed using the constants defined in {@link PyBUF},
+     * clearing individual flags specified while leaving others already set. Equivalent to
+     * <code>setFeatureFlags(~flags & getFeatureFlags())</code>.
+     *
+     * @param flags to clear within the feature flags
+     */
+    protected final void removeFeatureFlags(int flags) {
+        setFeatureFlags(~flags & getFeatureFlags());
+    }
+
+    /**
      * General purpose method to check the consumer request flags (typically the argument to
      * {@link BufferProtocol#getBuffer(int)}) against the feature flags (see
      * {@link #getFeatureFlags()}) that characterise the features of the buffer, and to raise an
@@ -187,7 +203,7 @@
      * capabilities this type (or instance) buffer actually has. It is an error, for the consumer to
      * specify in its request a feature that the buffer does not offer.
      * <p>
-     * In a subset of the flags, the consumer specifies the set of navigational arrays (
+     * In a subset of the flags, the consumer specifies the set of navigation arrays (
      * <code>shape</code>, <code>strides</code>, and <code>suboffsets</code>) it intends to use in
      * navigating the buffer. When the buffer implementation calls this check method, it has already
      * specified in {@link #setFeatureFlags(int)} what navigation is necessary for the consumer to
@@ -210,14 +226,8 @@
 
     @Override
     public boolean isReadonly() {
-        // WRITABLE is a non-navigational flag, so is inverted in gFeatureFlags
-        return (gFeatureFlags & WRITABLE) != 0;
-    }
-
-    @Override
-    public boolean hasArray() {
-        // AS_ARRAY is a non-navigational flag, so is inverted in gFeatureFlags
-        return (gFeatureFlags & AS_ARRAY) != 0;
+        // WRITABLE is a non-navigation flag, so is inverted in gFeatureFlags
+        return (gFeatureFlags & WRITABLE) != 0; // i.e. featureFlags & WRITABLE is false
     }
 
     @Override
@@ -231,91 +241,155 @@
         return shape;
     }
 
+    // XXX Consider making this part of the PyBUF interface
+    protected int getSize() {
+        final int N = shape.length;
+        int size = shape[0];
+        for (int k = 1; k < N; k++) {
+            size *= shape[k];
+        }
+        return size;
+    }
+
+    @Override
+    public int getLen() {
+        final int N = shape.length;
+        int len = getItemsize();
+        for (int k = 0; k < N; k++) {
+            len *= shape[k];
+        }
+        return len;
+    }
+
+    @Override
+    public final BufferProtocol getObj() {
+        return obj;
+    }
+
+    /**
+     * Retrieve the byte at the given index in the underlying storage treated as a flat sequence of
+     * bytes. This byte-index will have been computed from the item index (which may have been
+     * multi-dimensional), taking into account {@link #index0}, {@link #shape}, {@link #strides},
+     * and the item size. The caller is responsible for validating the original item-index and
+     * raising (typically) an <code>IndexOutOfBoundsException</code>. Misuse of this method may
+     * still result in unchecked exceptions characteristic of the storage implementation.
+     *
+     * @param byteIndex byte-index of location to retrieve
+     * @return the byte at byteIndex
+     */
+    abstract protected byte byteAtImpl(int byteIndex) throws IndexOutOfBoundsException;
+
+    /**
+     * Store the byte at the given index in the underlying storage treated as a flat sequence of
+     * bytes. This byte-index will have been computed from the item index (which may have been
+     * multi-dimensional), taking into account {@link #index0}, {@link #shape}, {@link #strides},
+     * and the item size. The caller is responsible for validating the original item-index and
+     * raising (typically) an <code>IndexOutOfBoundsException</code>. Misuse of this method may
+     * still result in unchecked exceptions characteristic of the storage implementation. This
+     * method must implement the check for read-only character, raising a <code>BufferError</code>
+     * in the case of a violation.
+     *
+     * @param value to store
+     * @param byteIndex byte-index of location to retrieve
+     * @throws PyException(BufferError) if this object is read-only.
+     */
+    abstract protected void storeAtImpl(byte value, int byteIndex)
+            throws IndexOutOfBoundsException, PyException;
+
     /**
      * {@inheritDoc}
      * <p>
-     * The default implementation in <code>BaseBuffer</code> deals with the general one-dimensional
-     * case, with any item size and stride.
+     * The <code>BaseBuffer</code> implementation delegates to {@link #byteAtImpl(int)} via
+     * <code>byteAtImpl(byteIndex(index))</code>.
      */
     @Override
-    public int getLen() {
-        // Correct if one-dimensional. Override with itemsize*product(shape).
-        return shape[0] * getItemsize();
-    }
-
-    @Override
     public byte byteAt(int index) throws IndexOutOfBoundsException {
-        return storage[calcIndex(index)];
-    }
-
-    @Override
-    public int intAt(int index) throws IndexOutOfBoundsException {
-        return 0xff & byteAt(index);
-    }
-
-    @Override
-    public void storeAt(byte value, int index) throws IndexOutOfBoundsException, PyException {
-        if (isReadonly()) {
-            throw notWritable();
-        }
-        storage[calcIndex(index)] = value;
+        return byteAtImpl(byteIndex(index));
     }
 
     /**
-     * Convert an item index (for a one-dimensional buffer) to an absolute byte index in the actual
-     * storage being shared by the exporter. See {@link #calcIndex(int...)} for discussion.
-     *
-     * @param index from consumer
-     * @return index in actual storage
+     * {@inheritDoc}
+     * <p>
+     * The <code>BaseBuffer</code> implementation delegates to {@link #byteAtImpl(int)} via
+     * <code>byteAtImpl(byteIndex(index))</code>, cast unsigned to an <code>int</code>.
      */
-    protected int calcIndex(int index) throws IndexOutOfBoundsException {
-        // Treat as one-dimensional
-        return index0 + index * getStrides()[0];
+    @Override
+    public int intAt(int index) throws IndexOutOfBoundsException {
+        return 0xff & byteAtImpl(byteIndex(index));
     }
 
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The <code>BaseBuffer</code> implementation delegates to {@link #storeAtImpl(byte, int)} via
+     * <code>storeAtImpl(value, byteIndex(index))</code>.
+     */
+    @Override
+    public void storeAt(byte value, int index) throws IndexOutOfBoundsException, PyException {
+        storeAtImpl(value, byteIndex(index));
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The <code>BaseBuffer</code> implementation delegates to {@link #byteAtImpl(int)} via
+     * <code>byteAtImpl(byteIndex(indices))</code>.
+     */
     @Override
     public byte byteAt(int... indices) throws IndexOutOfBoundsException {
-        return storage[calcIndex(indices)];
+        return byteAtImpl(byteIndex(indices));
     }
 
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The <code>BaseBuffer</code> implementation delegates to {@link #byteAtImpl(int)} via
+     * <code>byteAtImpl(byteIndex(indices))</code>, cast unsigned to an <code>int</code>.
+     */
     @Override
     public int intAt(int... indices) throws IndexOutOfBoundsException {
         return 0xff & byteAt(indices);
     }
 
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The <code>BaseBuffer</code> implementation delegates to {@link #storeAtImpl(byte, int)} via
+     * <code>storeAtImpl(value, byteIndex(indices))</code>.
+     */
     @Override
     public void storeAt(byte value, int... indices) throws IndexOutOfBoundsException, PyException {
-        if (isReadonly()) {
-            throw notWritable();
-        }
-        storage[calcIndex(indices)] = value;
+        storeAtImpl(value, byteIndex(indices));
     }
 
-    /**
-     * Convert a multi-dimensional item index (if we are not using indirection) to an absolute byte
-     * index in the actual storage array being shared by the exporter. The purpose of this method is
-     * to allow a sub-class to define, in one place, an indexing calculation that maps the index as
-     * provided by the consumer into an index in the storage known to the buffer.
-     * <p>
-     * In the usual case where the storage is referenced via the {@link #storage} and
-     * {@link #index0} members, the buffer implementation may use <code>storage[calcIndex(i)]</code>
-     * to reference the (first byte of) the item x[i]. This is what the default implementation of
-     * accessors in <code>BaseBuffer</code> will do. In the simplest cases, calling
-     * <code>calcIndex</code> may be an overhead to avoid, and an implementation will specialise the
-     * accessors. The default implementation here is suited to N-dimensional arrays.
-     *
-     * @param indices of the item from the consumer
-     * @return corresponding absolute index in storage
+    /*
+     * In this implementation, we throw IndexOutOfBoundsException if index < 0 or > shape[0], but we
+     * could rely on the array or ByteBuffer checks when indexing, especially the latter since
+     * position is checked against limit.
      */
-    protected int calcIndex(int... indices) throws IndexOutOfBoundsException {
+    @Override
+    public int byteIndex(int index) throws IndexOutOfBoundsException {
+        // Treat as one-dimensional
+        if (index < 0 || index >= shape[0]) {
+            throw new IndexOutOfBoundsException();
+        }
+        return index0 + index * strides[0];
+    }
+
+    /*
+     * In this implementation, we throw IndexOutOfBoundsException if any index[i] < 0 or > shape[i].
+     */
+    @Override
+    public int byteIndex(int... indices) throws IndexOutOfBoundsException {
         final int N = checkDimension(indices);
         // In general: index0 + sum(k=0,N-1) indices[k]*strides[k]
         int index = index0;
-        if (N > 0) {
-            int[] strides = getStrides();
-            for (int k = 0; k < N; k++) {
-                index += indices[k] * strides[k];
+        for (int k = 0; k < N; k++) {
+            int ik = indices[k];
+            if (ik < 0 || ik >= shape[k]) {
+                throw new IndexOutOfBoundsException();
             }
+            index += ik * strides[k];
         }
         return index;
     }
@@ -323,24 +397,22 @@
     /**
      * Calculate the absolute byte index in the storage array of the last item of the exported data
      * (if we are not using indirection). This is the greatest value attained by
-     * {@link #calcIndex(int...)}. The first byte not used will be one <code>itemsize</code> more
+     * {@link #byteIndex(int...)}. The first byte not used will be one <code>itemsize</code> more
      * than the returned value.
      *
      * @return greatest absolute index in storage
      */
-    protected int calcGreatestIndex() throws IndexOutOfBoundsException {
+    protected int calcGreatestIndex() {
         final int N = shape.length;
         // If all the strides are positive, the maximal value is found from:
         // index = index0 + sum(k=0,N-1) (shape[k]-1)*strides[k]
         // but in general, for any k where strides[k]<=0, the term should be zero.
         int index = index0;
-        if (N > 0) {
-            int[] strides = getStrides();
-            for (int k = 0; k < N; k++) {
-                int stride = strides[k];
-                if (stride > 0) {
-                    index += (shape[k] - 1) * stride;
-                }
+        int[] strides = getStrides();
+        for (int k = 0; k < N; k++) {
+            int stride = strides[k];
+            if (stride > 0) {
+                index += (shape[k] - 1) * stride;
             }
         }
         return index;
@@ -349,23 +421,21 @@
     /**
      * Calculate the absolute byte index in the storage array of the first item of the exported data
      * (if we are not using indirection). This is the least value attained by
-     * {@link #calcIndex(int...)}.
+     * {@link #byteIndex(int...)}.
      *
      * @return least absolute index in storage
      */
-    protected int calcLeastIndex() throws IndexOutOfBoundsException {
+    protected int calcLeastIndex() {
         final int N = shape.length;
         // If all the strides are positive, the maximal value is just index0,
         // but in general, we must allow strides[k]<=0 for some k:
         // index = index0 + sum(k=0,N-1) (strides[k]<0) ? (shape[k]-1)*strides[k] : 0
         int index = index0;
-        if (N > 0) {
-            int[] strides = getStrides();
-            for (int k = 0; k < N; k++) {
-                int stride = strides[k];
-                if (stride < 0) {
-                    index += (shape[k] - 1) * stride;
-                }
+        int[] strides = getStrides();
+        for (int k = 0; k < N; k++) {
+            int stride = strides[k];
+            if (stride < 0) {
+                index += (shape[k] - 1) * stride;
             }
         }
         return index;
@@ -380,147 +450,101 @@
     @Override
     public void copyTo(byte[] dest, int destPos) throws IndexOutOfBoundsException {
         // Note shape[0] is the number of items in the array
-        copyTo(0, dest, destPos, shape[0]);
+        copyTo(0, dest, destPos, getSize());
     }
 
     /**
      * {@inheritDoc}
      * <p>
      * The default implementation in <code>BaseBuffer</code> deals with the general one-dimensional
-     * case of arbitrary item size and stride.
+     * case of arbitrary item size and stride, but is unable to optimise access to sequential bytes.
      */
     @Override
-    public void copyTo(int srcIndex, byte[] dest, int destPos, int length)
-            throws IndexOutOfBoundsException {
+    public void copyTo(int srcIndex, byte[] dest, int destPos, int count)
+            throws IndexOutOfBoundsException, PyException {
 
-        // Data is here in the buffers
-        int s = calcIndex(srcIndex);
-        int d = destPos;
+        checkDimension(1);
 
-        // Pick up attributes necessary to choose an efficient copy strategy
         int itemsize = getItemsize();
-        int stride = getStrides()[0];
-        int skip = stride - itemsize;
+        int s = srcIndex, d = destPos;
 
-        // Strategy depends on whether items are laid end-to-end contiguously or there are gaps
-        if (skip == 0) {
-            // stride == itemsize: straight copy of contiguous bytes
-            System.arraycopy(storage, s, dest, d, length * itemsize);
-
-        } else if (itemsize == 1) {
-            // Non-contiguous copy: single byte items
-            int limit = s + length * stride;
-            for (; s < limit; s += stride) {
-                dest[d++] = storage[s];
+        if (itemsize == 1) {
+            // Single byte items
+            for (int i = 0; i < count; i++) {
+                dest[d++] = byteAt(s++);
             }
-
         } else {
-            // Non-contiguous copy: each time, copy itemsize bytes then skip
-            int limit = s + length * stride;
-            for (; s < limit; s += skip) {
-                int t = s + itemsize;
-                while (s < t) {
-                    dest[d++] = storage[s++];
+            // Multi-byte items
+            for (int i = 0; i < count; i++) {
+                int p = byteIndex(s++);
+                for (int j = 0; j < itemsize; j++) {
+                    dest[d++] = byteAtImpl(p + j);
                 }
             }
         }
-
     }
 
     /**
      * {@inheritDoc}
      * <p>
      * The default implementation in <code>BaseBuffer</code> deals with the general one-dimensional
-     * case of arbitrary item size and stride.
+     * case of arbitrary item size and stride, but is unable to optimise access to sequential bytes.
      */
     @Override
-    public void copyFrom(byte[] src, int srcPos, int destIndex, int length)
+    public void copyFrom(byte[] src, int srcPos, int destIndex, int count)
             throws IndexOutOfBoundsException, PyException {
 
-        // Block operation if read-only
-        if (isReadonly()) {
-            throw notWritable();
-        }
+        checkDimension(1);
+        checkWritable();
 
-        // Data is here in the buffers
-        int s = srcPos;
-        int d = calcIndex(destIndex);
+        int itemsize = getItemsize();
+        int d = destIndex, s = srcPos;
 
-        // Pick up attributes necessary to choose an efficient copy strategy
-        int itemsize = getItemsize();
-        int stride = getStrides()[0];
-        int skip = stride - itemsize;
-
-        // Strategy depends on whether items are laid end-to-end or there are gaps
-        if (skip == 0) {
-            // Straight copy of contiguous bytes
-            System.arraycopy(src, srcPos, storage, d, length * itemsize);
-
-        } else if (itemsize == 1) {
-            // Non-contiguous copy: single byte items
-            int limit = d + length * stride;
-            for (; d != limit; d += stride) {
-                storage[d] = src[s++];
+        if (itemsize == 1) {
+            // Single byte items
+            for (int i = 0; i < count; i++) {
+                storeAt(src[s++], d++);
             }
-
         } else {
-            // Non-contiguous copy: each time, copy itemsize bytes then skip
-            int limit = d + length * stride;
-            for (; d != limit; d += skip) {
-                int t = d + itemsize;
-                while (d < t) {
-                    storage[d++] = src[s++];
+            // Multi-byte items
+            for (int i = 0; i < count; i++) {
+                int p = byteIndex(d++);
+                for (int j = 0; j < itemsize; j++) {
+                    storeAtImpl(src[s++], p++);
                 }
             }
         }
-
     }
 
     /**
      * {@inheritDoc}
      * <p>
      * The default implementation in <code>BaseBuffer</code> deals with the general one-dimensional
-     * case.
+     * case of arbitrary item size and stride, but is unable to optimise access to sequential bytes.
      */
     @Override
     public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException {
 
-        // Block operation if read-only and same length
-        if (isReadonly()) {
-            throw notWritable();
-        } else if (src.getLen() != getLen() || src.getItemsize() != getItemsize()) {
+        checkDimension(1);
+        checkWritable();
+
+        int itemsize = getItemsize();
+        int count = getSize();
+        int byteLen = src.getLen();
+
+        // Block operation if different item or overall size (permit reshape)
+        if (src.getItemsize() != itemsize || byteLen != count * itemsize) {
             throw differentStructure();
         }
 
-        // Data is here in the buffers
-        int s = 0;
-        int d = calcIndex(0);
-
-        // Pick up attributes necessary to choose an efficient copy strategy
-        int itemsize = getItemsize();
-        int stride = getStrides()[0];
-
-        // Strategy depends on whether items are laid end-to-end or there are gaps
-        if (stride == itemsize) {
-            // Straight copy to contiguous bytes
-            src.copyTo(storage, d);
-
-        } else if (itemsize == 1) {
-            // Non-contiguous copy: single byte items
-            int limit = d + src.getLen() * stride;
-            for (; d != limit; d += stride) {
-                storage[d] = src.byteAt(s++);
-            }
-
-        } else {
-            // Non-contiguous copy: each time, copy itemsize bytes then skip
-            int limit = d + src.getShape()[0] * stride;
-            for (; d != limit; d += stride) {
-                Pointer srcItem = src.getPointer(s++);
-                System.arraycopy(srcItem.storage, srcItem.offset, storage, d, itemsize);
-            }
-        }
-
+        /*
+         * It is not possible in general to know that this and src do not share storage. There is
+         * always a risk of incorrect results if we do not go via an intermediate byte array.
+         * Sub-classes may be able to avoid this.
+         */
+        byte[] t = new byte[byteLen];
+        src.copyTo(t, 0);
+        this.copyFrom(t, 0, 0, count);
     }
 
     @Override
@@ -535,7 +559,7 @@
     }
 
     /**
-     * Allow an exporter to re-use a BaseBytes even if it has been "finally" released. Many
+     * Allow an exporter to re-use this object again even if it has been "finally" released. Many
      * sub-classes of <code>BaseBytes</code> can be re-used even after a final release by consumers,
      * simply by incrementing the <code>exports</code> count again: the navigation arrays and the
      * buffer view of the exporter's state all remain valid. We do not let consumers do this through
@@ -567,12 +591,22 @@
      * equals the number of <code>getBuffer</code> calls), the implementation here calls
      * {@link #releaseAction()}, which the implementer of a specific buffer type should override if
      * it needs specific actions to take place.
+     * <p>
+     * Note that, when this is a sliced view obtained from another <code>PyBuffer</code> the
+     * implementation in <code>BaseBuffer</code> automatically sends one <code>release()</code>
+     * Sub-classes should not propagate the release themselves when overriding
+     * {@link #releaseAction()}.
      */
     @Override
     public void release() {
         if (--exports == 0) {
             // This is a final release.
             releaseAction();
+            // We have to release the root too if we are not a root.
+            PyBuffer root = getRoot();
+            if (root != this) {
+                root.release();
+            }
         } else if (exports < 0) {
             // Buffer already had 0 exports. (Put this right, in passing.)
             exports = 0;
@@ -591,35 +625,63 @@
     }
 
     @Override
-    public PyBuffer getBufferSlice(int flags, int start, int length) {
-        return getBufferSlice(flags, start, length, 1);
+    public PyBuffer getBufferSlice(int flags, int start, int count) {
+        return getBufferSlice(flags, start, count, 1);
     }
 
     // Let the sub-class implement
-    // @Override public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {}
+    // @Override public PyBuffer getBufferSlice(int flags, int start, int count, int stride) {}
+
+    /**
+     * Create a new <code>java.nio.ByteBuffer</code> on the underlying storage, such that
+     * positioning this buffer to a particular byte using {@link #byteIndex(int)} or
+     * {@link #byteIndex(int[])} positions it at the first byte of the item so indexed.
+     */
+    abstract protected ByteBuffer getNIOByteBufferImpl();
 
     @Override
     public ByteBuffer getNIOByteBuffer() {
-        // Determine the limit of the buffer just beyond the last item.
-        int length = calcGreatestIndex() + getItemsize() - index0;
-        ByteBuffer b = ByteBuffer.wrap(storage, index0, length);
-        // Return as read-only if it is.
-        return isReadonly() ? b.asReadOnlyBuffer() : b;
+        // The buffer spans the whole storage
+        ByteBuffer b = getNIOByteBufferImpl();
+        // For the one-dimensional contiguous case it makes sense to set the limit:
+        if (shape.length == 1 && isContiguous('A')) {
+            int stride = strides[0];
+            if (getItemsize() == stride) {
+                b.limit(index0 + shape[0] * stride);
+            }
+        }
+        // The buffer is positioned at item[0]
+        b.position(index0);
+        return b;
     }
 
     @Override
-    public Pointer getBuf() {
-        return new Pointer(storage, index0);
+    public boolean hasArray() {
+        // AS_ARRAY is a non-navigation flag, so is inverted in gFeatureFlags
+        return (gFeatureFlags & AS_ARRAY) == 0; // i.e. featureFlags & AS_ARRAY is true
     }
 
+    @SuppressWarnings("deprecation")
+    @Override
+    public Pointer getBuf() {
+        checkHasArray();
+        return new Pointer(getNIOByteBuffer().array(), index0);
+    }
+
+    @SuppressWarnings("deprecation")
     @Override
     public Pointer getPointer(int index) throws IndexOutOfBoundsException {
-        return new Pointer(storage, calcIndex(index));
+        Pointer p = getBuf();
+        p.offset = byteIndex(index);
+        return p;
     }
 
+    @SuppressWarnings("deprecation")
     @Override
     public Pointer getPointer(int... indices) throws IndexOutOfBoundsException {
-        return new Pointer(storage, calcIndex(indices));
+        Pointer p = getBuf();
+        p.offset = byteIndex(indices);
+        return p;
     }
 
     @Override
@@ -633,10 +695,73 @@
         return null;
     }
 
+    private boolean isCContiguous() {
+        /*
+         * If we were to compute the strides array for a C-contiguous array, the last stride would
+         * equal the item size, and generally stride[k-1] = shape[k]*stride[k]. This is the basis of
+         * the test. However, note that for any k where shape[k]==1 there is no "next sub-array" and
+         * no discontiguity.
+         */
+        final int N = shape.length;
+        /*
+         * size is the stride in bytes-index from item[i0,i1,...,ik,0,...,0] to
+         * item[i0,i1,...,ik+1,0,...,0]. Start the iteration at the largest k. An increment of one
+         * in the last index makes a stride of the item size.
+         */
+        int size = getItemsize();
+        for (int k = N - 1; k >= 0; k--) {
+            int nk = shape[k];
+            if (nk > 1) {
+                if (strides[k] != size) {
+                    return false;
+                }
+                size *= nk;
+            }
+        }
+        return true;
+    }
+
+    private boolean isFortranContiguous() {
+        /*
+         * If we were to compute the strides array for a Fortran-contiguous array, the first stride
+         * would equal the item size, and generally stride[k+1] = shape[k]*stride[k]. This is the
+         * basis of the test. However, note that for any k where shape[k]==1 there is no
+         * "next sub-array" and no discontiguity.
+         */
+        final int N = shape.length;
+        /*
+         * size is the stride in bytes-index from item[0,...,0,ik,0,...,0] to
+         * item[0,...,0,ik+1,0,...,0]. Start the iteration at k=0. An increment of one in the first
+         * index makes a stride of the item size.
+         */
+        int size = getItemsize();
+        for (int k = 0; k < N; k++) {
+            int nk = shape[k];
+            if (nk > 1) {
+                if (strides[k] != size) {
+                    return false;
+                }
+                size *= nk;
+            }
+        }
+        return true;
+    }
+
     @Override
     public boolean isContiguous(char order) {
-        // Correct for one-dimensional buffers
-        return true;
+        if (getSuboffsets() != null) {
+            return false;
+        }
+        switch (order) {
+            case 'C':
+                return isCContiguous();
+            case 'F':
+                return isFortranContiguous();
+            case 'A':
+                return isCContiguous() || isFortranContiguous();
+            default:
+                return false;
+        }
     }
 
     @Override
@@ -662,9 +787,9 @@
     protected void releaseAction() {}
 
     /**
-     * Some <code>PyBuffer</code>s, those created by slicing a <code>PyBuffer</code> are related to
+     * Some <code>PyBuffer</code>s, those created by slicing a <code>PyBuffer</code>, are related to
      * a root <code>PyBuffer</code>. During creation of such a slice, we need to supply a value for
-     * this root. If the present object is not itself a slice, this is root is the object itself; if
+     * this root. If the present object is not itself a slice, this root is the object itself; if
      * the buffer is already a slice, it is the root it was given at creation time. Often this is
      * the only difference between a slice-view and a directly-exported buffer. Override this method
      * in slices to return the root buffer of the slice.
@@ -676,6 +801,20 @@
     }
 
     /**
+     * The toString() method of a buffer reproduces the values in the buffer (as unsigned integers)
+     * as the character codes of a <code>String</code>.
+     */
+    @Override
+    public String toString() {
+        int n = getLen();
+        StringBuilder sb = new StringBuilder(n);
+        for (int i = 0; i < n; i++) {
+            sb.appendCodePoint(intAt(i));
+        }
+        return sb.toString();
+    }
+
+    /**
      * Check the number of indices (but not their values), raising a Python BufferError if this does
      * not match the number of dimensions. This is a helper for N-dimensional arrays.
      *
@@ -707,17 +846,25 @@
     }
 
     /**
-     * The toString() method of a buffer reproduces the values in the buffer (as unsigned integers)
-     * as the character codes of a <code>String</code>.
+     * Check that the buffer is writable.
+     *
+     * @throws PyException (TypeError) if not
      */
-    @Override
-    public String toString() {
-        int n = getLen();
-        StringBuilder sb = new StringBuilder(n);
-        for (int i = 0; i < n; i++) {
-            sb.appendCodePoint(intAt(i));
+    protected void checkWritable() throws PyException {
+        if (isReadonly()) {
+            throw notWritable();
         }
-        return sb.toString();
+    }
+
+    /**
+     * Check that the buffer is backed by an array the client can access as byte[].
+     *
+     * @throws PyException (BufferError) if not
+     */
+    protected void checkHasArray() throws PyException {
+        if (!hasArray()) {
+            throw bufferIsNot("accessible as a Java array");
+        }
     }
 
     /**
diff --git a/src/org/python/core/buffer/BaseNIOBuffer.java b/src/org/python/core/buffer/BaseNIOBuffer.java
new file mode 100644
--- /dev/null
+++ b/src/org/python/core/buffer/BaseNIOBuffer.java
@@ -0,0 +1,249 @@
+package org.python.core.buffer;
+
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.ReadOnlyBufferException;
+
+import org.python.core.PyBUF;
+import org.python.core.PyException;
+
+/**
+ * Base implementation of the Buffer API for when the storage implementation is
+ * <code>java.nio.ByteBuffer</code>. The description of {@link BaseBuffer} mostly applies. Methods
+ * provided or overridden here are appropriate to 1-dimensional arrays, of any item size, backed by
+ * a <code>ByteBuffer</code>.
+ */
+public abstract class BaseNIOBuffer extends Base1DBuffer {
+
+    /**
+     * A {@link java.nio.ByteBuffer} (possibly a direct buffer) wrapping the storage that the
+     * exporter is sharing with the consumer. The data to be exposed may be only a subset of the
+     * bytes in the buffer, defined by the navigation information <code>index0</code>,
+     * <code>shape</code>, <code>strides</code>, etc., usually defined in the constructor.
+     * <p>
+     * Implementations must not adjust the position and limit of <code>storage</code> after
+     * construction. It will generally be a duplicate of (not a reference to) a ByteBuffer held by
+     * the client code. The capacity and backing store are fixed in construction, and the position
+     * will always be {@link #index0}. The limit is always higher than any valid data, and in the
+     * case of a contiguous buffer (with positive stride), is exactly just beyond the last item, so
+     * that a series of ByteBuffer.get operations will yield the data.
+     */
+    protected ByteBuffer storage;
+
+    /**
+     * Construct an instance of <code>BaseNIOBuffer</code> in support of a sub-class, specifying the
+     * 'feature flags', or at least a starting set to be adjusted later. Also specify the navigation
+     * ( {@link #index0}, number of elements, and stride. These 'feature flags' are the features of
+     * the buffer exported, not the flags that form the consumer's request. The buffer will be
+     * read-only unless {@link PyBUF#WRITABLE} is set. {@link PyBUF#FORMAT} and
+     * {@link PyBUF#AS_ARRAY} are implicitly added to the feature flags.
+     * <p>
+     * To complete initialisation, the sub-class normally must call {@link #checkRequestFlags(int)}
+     * passing the consumer's request flags.
+     *
+     * @param storage the <code>ByteBuffer</code> wrapping the exported object state. NOTE: this
+     *            <code>PyBuffer</code> keeps a reference and may manipulate the position, mark and
+     *            limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy.
+     * @param featureFlags bit pattern that specifies the features allowed
+     * @param index0 index into storage of <code>item[0]</code>
+     * @param size number of elements in the view
+     * @param stride byte-index step between successive elements
+     */
+    protected BaseNIOBuffer(ByteBuffer storage, int featureFlags, int index0, int size, int stride) {
+        super(featureFlags & ~(WRITABLE | AS_ARRAY), index0, size, stride);
+        this.storage = storage;
+
+        // Deduce other feature flags from the client's ByteBuffer
+        if (!storage.isReadOnly()) {
+            addFeatureFlags(WRITABLE);
+        }
+        if (storage.hasArray()) {
+            addFeatureFlags(AS_ARRAY);
+        }
+    }
+
+    @Override
+    protected byte byteAtImpl(int byteIndex) throws IndexOutOfBoundsException {
+        return storage.get(byteIndex);
+    }
+
+    @Override
+    protected void storeAtImpl(byte value, int byteIndex) throws PyException {
+        try {
+            storage.put(byteIndex, value);
+        } catch (ReadOnlyBufferException rbe) {
+            throw notWritable();
+        }
+    }
+
+    @Override
+    public int byteIndex(int... indices) throws IndexOutOfBoundsException {
+        // BaseBuffer implementation can be simplified since if indices.length!=1 we error.
+        checkDimension(indices.length); // throws if != 1
+        return byteIndex(indices[0]);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation in <code>BaseNIOBuffer</code> deals with the general
+     * one-dimensional case of arbitrary item size and stride.
+     */
+    @Override
+    public void copyTo(int srcIndex, byte[] dest, int destPos, int count)
+            throws IndexOutOfBoundsException {
+        // Wrap the destination, taking care to reflect the necessary range we shall write.
+        ByteBuffer destBuf = ByteBuffer.wrap(dest, destPos, count * getItemsize());
+        copyTo(srcIndex, destBuf, count);
+    }
+
+    /**
+     * Copy all items in this buffer into a <code>ByteBuffer</code>, starting at its current
+     * position.
+     *
+     * @param dest destination buffer
+     * @throws BufferOverflowException
+     * @throws ReadOnlyBufferException
+     */
+    // XXX Should this become part of the PyBUffer interface?
+    public void copyTo(ByteBuffer dest) throws BufferOverflowException, ReadOnlyBufferException {
+        // Note shape[0] is the number of items in the buffer
+        copyTo(0, dest, shape[0]);
+    }
+
+    /**
+     * Copy a specified number of items from a particular location in this buffer into a
+     * <code>ByteBuffer</code>, starting at its current position. .
+     *
+     * @param srcIndex index of the first item to copy
+     * @param dest destination buffer
+     * @param count number of items to copy
+     * @throws BufferOverflowException
+     * @throws ReadOnlyBufferException
+     * @throws IndexOutOfBoundsException
+     */
+    // XXX Should this become part of the PyBuffer interface?
+    protected void copyTo(int srcIndex, ByteBuffer dest, int count) throws BufferOverflowException,
+            ReadOnlyBufferException, IndexOutOfBoundsException {
+
+        if (count > 0) {
+
+            ByteBuffer src = getNIOByteBuffer();
+            int pos = byteIndex(srcIndex);
+
+            // Pick up attributes necessary to choose an efficient copy strategy
+            int itemsize = getItemsize();
+            int stride = getStrides()[0];
+
+            // Strategy depends on whether items are laid end-to-end contiguously or there are gaps
+            if (stride == itemsize) {
+                // stride == itemsize: straight copy of contiguous bytes
+                src.limit(pos + count * itemsize).position(pos);
+                dest.put(src);
+
+            } else if (itemsize == 1) {
+                // Non-contiguous copy: single byte items
+                for (int i = 0; i < count; i++) {
+                    src.position(pos);
+                    dest.put(src.get());
+                    pos += stride;
+                }
+
+            } else {
+                // Non-contiguous copy: each time, copy itemsize bytes then skip
+                for (int i = 0; i < count; i++) {
+                    src.limit(pos + itemsize).position(pos);
+                    dest.put(src);
+                    pos += stride;
+                }
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation in <code>BaseNIOBuffer</code> deals with the general
+     * one-dimensional case of arbitrary item size and stride.
+     */
+    @Override
+    public void copyFrom(byte[] src, int srcPos, int destIndex, int count)
+            throws IndexOutOfBoundsException, PyException {
+        // Wrap the source, taking care to reflect the range we shall read.
+        ByteBuffer srcBuf = ByteBuffer.wrap(src, srcPos, count * getItemsize());
+        copyFrom(srcBuf, destIndex, count);
+    }
+
+    /**
+     * Copy a specified number of items from a <code>ByteBuffer</code> into this buffer at a
+     * particular location.
+     *
+     * @param src source <code>ByteBuffer</code>
+     * @param destIndex starting item-index in the destination (i.e. <code>this</code>)
+     * @param count number of items to copy in
+     * @throws IndexOutOfBoundsException if access out of bounds in source or destination
+     * @throws PyException (TypeError) if read-only buffer
+     */
+    // XXX Should this become part of the PyBUffer interface?
+    protected void copyFrom(ByteBuffer src, int destIndex, int count)
+            throws IndexOutOfBoundsException, PyException {
+
+        checkWritable();
+
+        if (count > 0) {
+
+            ByteBuffer dest = getNIOByteBuffer();
+            int pos = byteIndex(destIndex);
+
+            // Pick up attributes necessary to choose an efficient copy strategy
+            int itemsize = getItemsize();
+            int stride = getStrides()[0];
+            int skip = stride - itemsize;
+            int size = getSize();
+
+            // Check indexes in destination (this) using the "all non-negative" trick
+            if ((destIndex | count | size - (destIndex + count)) < 0) {
+                throw new IndexOutOfBoundsException();
+            }
+
+            // Strategy depends on whether items are laid end-to-end or there are gaps
+            if (skip == 0) {
+                // Straight copy of contiguous bytes
+                dest.position(pos);
+                dest.put(src);
+
+            } else if (itemsize == 1) {
+                // Non-contiguous copy: single byte items
+                for (int i = 0; i < count; i++) {
+                    dest.position(pos);
+                    dest.put(src.get());
+                    // Next byte written will be here
+                    pos += stride;
+                }
+
+            } else {
+                // Non-contiguous copy: each time, copy itemsize bytes at a time
+                for (int i = 0; i < count; i++) {
+                    dest.position(pos);
+                    // Delineate the next itemsize bytes in the src
+                    src.limit(src.position() + itemsize);
+                    dest.put(src);
+                    // Next byte written will be here
+                    pos += stride;
+                }
+            }
+        }
+    }
+
+    @Override
+    protected ByteBuffer getNIOByteBufferImpl() {
+        return storage.duplicate();
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    public Pointer getBuf() {
+        checkHasArray();
+        return new Pointer(storage.array(), index0);
+    }
+}
diff --git a/src/org/python/core/buffer/SimpleBuffer.java b/src/org/python/core/buffer/SimpleBuffer.java
--- a/src/org/python/core/buffer/SimpleBuffer.java
+++ b/src/org/python/core/buffer/SimpleBuffer.java
@@ -1,7 +1,6 @@
 package org.python.core.buffer;
 
-import java.nio.ByteBuffer;
-
+import org.python.core.BufferProtocol;
 import org.python.core.PyBuffer;
 import org.python.core.PyException;
 import org.python.core.util.StringUtil;
@@ -9,59 +8,31 @@
 /**
  * Buffer API over a read-only one-dimensional array of one-byte items.
  */
-public class SimpleBuffer extends BaseBuffer {
-
-    /**
-     * The strides array for this type is always a single element array with a 1 in it.
-     */
-    protected static final int[] SIMPLE_STRIDES = {1};
-
-    /**
-     * Provide an instance of <code>SimpleBuffer</code> with navigation variables partly
-     * initialised, for sub-class use. One-dimensional arrays without strides are C- and
-     * F-contiguous. To complete initialisation, the sub-class must normally assign the buffer (
-     * {@link #storage}, {@link #index0}), and the navigation ({@link #shape} array), and then call
-     * {@link #checkRequestFlags(int)} passing the consumer's request flags.
-     */
-    protected SimpleBuffer() {
-        super(CONTIGUITY | SIMPLE);
-        // Initialise navigation
-        shape = new int[1];
-        strides = SIMPLE_STRIDES;
-        // suboffsets is always null for this type.
-    }
+public class SimpleBuffer extends BaseArrayBuffer {
 
     /**
      * Provide an instance of <code>SimpleBuffer</code> with navigation variables initialised, for
      * sub-class use. The buffer ({@link #storage}, {@link #index0}), and the {@link #shape} array
-     * will be initialised from the arguments (which are checked for range). The {@link #strides} is
-     * set for (one-byte) unit stride. Only the call to {@link #checkRequestFlags(int)}, passing the
-     * consumer's request flags really remains for the sub-class constructor to do.
+     * will be initialised from the arguments (which are not checked for range). The
+     * {@link #strides} is set for a one-byte stride. Only the call to
+     * {@link #checkRequestFlags(int)}, passing the consumer's request flags really remains for the
+     * sub-class constructor to do.
      *
      * <pre>
      * super(storage, index0, size);
      * checkRequestFlags(flags);        // Check request is compatible with type
      * </pre>
      *
+     * @param obj exporting object (or <code>null</code>)
      * @param storage the array of bytes storing the implementation of the exporting object
      * @param index0 offset where the data starts in that array (item[0])
      * @param size the number of bytes occupied
      * @throws NullPointerException if <code>storage</code> is null
-     * @throws ArrayIndexOutOfBoundsException if <code>index0</code> and <code>size</code> are
-     *             inconsistent with <code>storage.length</code>
      */
-    // XXX: "for sub-class use" = should be protected?
-    public SimpleBuffer(byte[] storage, int index0, int size) throws PyException,
-            ArrayIndexOutOfBoundsException {
-        this();
-        this.storage = storage;         // Exported data
-        this.index0 = index0;           // Index to be treated as item[0]
-        this.shape[0] = size;           // Number of items in exported data
-
-        // Check arguments using the "all non-negative" trick
-        if ((index0 | size | storage.length - (index0 + size)) < 0) {
-            throw new ArrayIndexOutOfBoundsException();
-        }
+    protected SimpleBuffer(BufferProtocol obj, byte[] storage, int index0, int size)
+            throws PyException, ArrayIndexOutOfBoundsException {
+        super(storage, CONTIGUITY | SIMPLE, index0, size, 1);
+        this.obj = obj;
     }
 
     /**
@@ -70,6 +41,7 @@
      * against the capabilities of the buffer type.
      *
      * @param flags consumer requirements
+     * @param obj exporting object (or <code>null</code>)
      * @param storage the array of bytes storing the implementation of the exporting object
      * @param index0 offset where the data starts in that array (item[0])
      * @param size the number of bytes occupied
@@ -78,10 +50,14 @@
      *             inconsistent with <code>storage.length</code>
      * @throws PyException (BufferError) when expectations do not correspond with the type
      */
-    public SimpleBuffer(int flags, byte[] storage, int index0, int size) throws PyException,
-            ArrayIndexOutOfBoundsException, NullPointerException {
-        this(storage, index0, size);    // Construct checked SimpleBuffer
-        checkRequestFlags(flags);       // Check request is compatible with type
+    public SimpleBuffer(int flags, BufferProtocol obj, byte[] storage, int index0, int size)
+            throws PyException, ArrayIndexOutOfBoundsException, NullPointerException {
+        this(obj, storage, index0, size);   // Construct checked SimpleBuffer
+        checkRequestFlags(flags);           // Check request is compatible with type
+        // Check arguments using the "all non-negative" trick
+        if ((index0 | size | storage.length - (index0 + size)) < 0) {
+            throw new ArrayIndexOutOfBoundsException();
+        }
     }
 
     /**
@@ -90,14 +66,12 @@
      * {@link #index0}), and the navigation ({@link #shape} array) will be initialised from the
      * array argument.
      *
+     * @param obj exporting object (or <code>null</code>)
      * @param storage the array of bytes storing the implementation of the exporting object
      * @throws NullPointerException if <code>storage</code> is null
      */
-    // XXX: "for sub-class use" = should be protected?
-    public SimpleBuffer(byte[] storage) throws NullPointerException {
-        this();
-        this.storage = storage;         // Exported data (index0=0 from initialisation)
-        this.shape[0] = storage.length; // Number of units in whole array
+    protected SimpleBuffer(BufferProtocol obj, byte[] storage) throws NullPointerException {
+        this(obj, storage, 0, storage.length);
     }
 
     /**
@@ -106,20 +80,17 @@
      * against the capabilities of the buffer type.
      *
      * @param flags consumer requirements
+     * @param obj exporting object (or <code>null</code>)
      * @param storage the array of bytes storing the implementation of the exporting object
      * @throws NullPointerException if <code>storage</code> is null
      * @throws PyException (BufferError) when expectations do not correspond with the type
      */
-    public SimpleBuffer(int flags, byte[] storage) throws PyException, NullPointerException {
-        this(storage);                  // Construct SimpleBuffer on whole array
+    public SimpleBuffer(int flags, BufferProtocol obj, byte[] storage) throws PyException,
+            NullPointerException {
+        this(obj, storage);             // Construct SimpleBuffer on whole array
         checkRequestFlags(flags);       // Check request is compatible with type
     }
 
-    @Override
-    public boolean isReadonly() {
-        return true;
-    }
-
     /**
      * {@inheritDoc}
      * <p>
@@ -135,72 +106,28 @@
     /**
      * {@inheritDoc}
      * <p>
-     * <code>SimpleBuffer</code> provides an implementation optimised for contiguous bytes in
-     * one-dimension.
+     * In <code>SimpleBuffer</code> the calculation is specialised for one dimension, no striding,
+     * and an item size of 1.
      */
     @Override
-    public byte byteAt(int index) throws IndexOutOfBoundsException {
-        // Implement directly: a bit quicker than the default
-        return storage[index0 + index];
-    }
-
-    /**
-     * {@inheritDoc}
-     * <p>
-     * <code>SimpleBuffer</code> provides an implementation optimised for contiguous bytes in
-     * one-dimension.
-     */
-    @Override
-    public int intAt(int index) throws IndexOutOfBoundsException {
-        // Implement directly: a bit quicker than the default
-        return 0xff & storage[index0 + index];
-    }
-
-    @Override
-    protected int calcIndex(int index) throws IndexOutOfBoundsException {
+    public int byteIndex(int index) throws IndexOutOfBoundsException {
+        if (index < 0 || index >= shape[0]) {
+            throw new IndexOutOfBoundsException();
+        }
         return index0 + index;
     }
 
-    /**
-     * {@inheritDoc}
-     * <p>
-     * <code>SimpleBuffer</code> provides an implementation optimised for contiguous bytes in
-     * one-dimension.
-     */
+    // XXX Consider moving to clauses in getBufferSlice(int, int, int, int)
+    // to avoid delegation loop where that delegates to this but in BaseBuffer the reverse.
     @Override
-    public byte byteAt(int... indices) throws IndexOutOfBoundsException {
-        checkDimension(indices.length);
-        return byteAt(indices[0]);
-    }
-
-    @Override
-    protected int calcIndex(int... indices) throws IndexOutOfBoundsException {
-        // BaseBuffer implementation can be simplified since if indices.length!=1 we error.
-        checkDimension(indices.length); // throws if != 1
-        return calcIndex(indices[0]);
-    }
-
-    /**
-     * {@inheritDoc}
-     * <p>
-     * <code>SimpleBuffer</code> provides an implementation optimised for contiguous bytes in
-     * one-dimension.
-     */
-    @Override
-    public void copyTo(int srcIndex, byte[] dest, int destPos, int length)
-            throws IndexOutOfBoundsException {
-        System.arraycopy(storage, index0 + srcIndex, dest, destPos, length);
-    }
-
-    @Override
-    public PyBuffer getBufferSlice(int flags, int start, int length) {
-        if (length > 0) {
+    public PyBuffer getBufferSlice(int flags, int start, int count) {
+        if (count > 0) {
             // Translate relative to underlying buffer
             int compIndex0 = index0 + start;
             // Create the slice from the sub-range of the buffer
-            return new SimpleView(getRoot(), flags, storage, compIndex0, length);
+            return new SimpleView(getRoot(), flags, storage, compIndex0, count);
         } else {
-            // Special case for length==0 where above logic would fail. Efficient too.
+            // Special case for count==0 where above logic would fail. Efficient too.
             return new ZeroByteBuffer.View(getRoot(), flags);
         }
     }
@@ -210,38 +137,33 @@
      * <p>
      * <code>SimpleBuffer</code> provides an implementation for slicing contiguous bytes in one
      * dimension. In that case, <i>x(i) = u(r+i)</i> for <i>i = 0..L-1</i> where u is the underlying
-     * buffer, and <i>r</i> and <i>L</i> are the start and length with which <i>x</i> was created
+     * buffer, and <i>r</i> and <i>L</i> are the start and count with which <i>x</i> was created
      * from <i>u</i>. Thus <i>y(k) = u(r+s+km)</i>, that is, the composite offset is <i>r+s</i> and
      * the stride is <i>m</i>.
      */
     @Override
-    public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {
+    public PyBuffer getBufferSlice(int flags, int start, int count, int stride) {
 
-        if (stride == 1 || length < 2) {
+        if (stride == 1 || count < 2) {
             // Unstrided slice of simple buffer is itself simple
-            return getBufferSlice(flags, start, length);
+            return getBufferSlice(flags, start, count);
 
         } else {
             // Translate relative to underlying buffer
             int compIndex0 = index0 + start;
             // Construct a view, taking a lock on the root object (this or this.root)
-            return new Strided1DBuffer.SlicedView(getRoot(), flags, storage, compIndex0, length,
+            return new Strided1DBuffer.SlicedView(getRoot(), flags, storage, compIndex0, count,
                     stride);
         }
     }
 
-    @Override
-    public ByteBuffer getNIOByteBuffer() {
-        // Simplify for one-dimensional contiguous bytes
-        ByteBuffer b = ByteBuffer.wrap(storage, index0, shape[0]);
-        return isReadonly() ? b.asReadOnlyBuffer() : b;
-    }
-
+    @SuppressWarnings("deprecation")
     @Override
     public Pointer getPointer(int index) throws IndexOutOfBoundsException {
         return new Pointer(storage, index0 + index);
     }
 
+    @SuppressWarnings("deprecation")
     @Override
     public Pointer getPointer(int... indices) throws IndexOutOfBoundsException {
         checkDimension(indices.length);
@@ -274,7 +196,7 @@
          */
         public SimpleView(PyBuffer root, int flags, byte[] storage, int offset, int size) {
             // Create a new SimpleBuffer on the buffer passed in (part of the root)
-            super(flags, storage, offset, size);
+            super(flags, root.getObj(), storage, offset, size);
             // Get a lease on the root PyBuffer
             this.root = root.getBuffer(FULL_RO);
         }
@@ -283,13 +205,5 @@
         protected PyBuffer getRoot() {
             return root;
         }
-
-        @Override
-        public void releaseAction() {
-            // We have to release the root too if ours was final.
-            root.release();
-        }
-
     }
-
 }
diff --git a/src/org/python/core/buffer/SimpleNIOBuffer.java b/src/org/python/core/buffer/SimpleNIOBuffer.java
new file mode 100644
--- /dev/null
+++ b/src/org/python/core/buffer/SimpleNIOBuffer.java
@@ -0,0 +1,190 @@
+package org.python.core.buffer;
+
+import java.nio.ByteBuffer;
+
+import org.python.core.BufferProtocol;
+import org.python.core.PyBuffer;
+import org.python.core.PyException;
+
+/**
+ * Buffer API over a read-only one-dimensional <code>java.nio.ByteBuffer</code> of one-byte items.
+ */
+public class SimpleNIOBuffer extends BaseNIOBuffer {
+
+    /**
+     * Provide an instance of <code>SimpleNIOBuffer</code> with navigation variables initialised,
+     * for sub-class use. The buffer ({@link #storage}, {@link #index0}), and the {@link #shape}
+     * array will be initialised from the arguments (which are checked for range). The
+     * {@link #strides} is set for (one-byte) unit stride. Only the call to
+     * {@link #checkRequestFlags(int)}, passing the consumer's request flags, really remains for the
+     * sub-class constructor to do.
+     *
+     * <pre>
+     * super(storage.duplicate(), index0, size);
+     * checkRequestFlags(flags);        // Check request is compatible with type
+     * </pre>
+     *
+     * @param obj exporting object (or <code>null</code>)
+     * @param storage the <code>ByteBuffer</code> wrapping the exported object state. NOTE: this
+     *            <code>PyBuffer</code> keeps a reference and may manipulate the position, mark and
+     *            limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy.
+     * @param index0 offset where the data starts in that array (item[0])
+     * @param size the number of bytes occupied
+     * @throws NullPointerException if <code>storage</code> is null
+     * @throws ArrayIndexOutOfBoundsException if <code>index0</code> and <code>size</code> are
+     *             inconsistent with <code>storage.capacity()</code>
+     */
+    protected SimpleNIOBuffer(BufferProtocol obj, ByteBuffer storage, int index0, int size)
+            throws PyException, ArrayIndexOutOfBoundsException {
+        super(storage, CONTIGUITY | SIMPLE, index0, size, 1);
+        this.obj = obj;
+        // Check arguments using the "all non-negative" trick
+        if ((index0 | size | storage.capacity() - (index0 + size)) < 0) {
+            throw new ArrayIndexOutOfBoundsException();
+        }
+    }
+
+    /**
+     * Provide an instance of <code>SimpleNIOBuffer</code>, on a slice of a {@link ByteBuffer},
+     * meeting the consumer's expectations as expressed in the <code>flags</code> argument, which is
+     * checked against the capabilities of the buffer type. No reference will be kept to the
+     * <code>ByteBuffer</code> passed in. (It is duplicated.)
+     *
+     * @param flags consumer requirements
+     * @param obj exporting object (or <code>null</code>)
+     * @param storage the <code>ByteBuffer</code> wrapping the exported object state
+     * @param index0 offset where the data starts in that buffer (item[0])
+     * @param size the number of bytes occupied
+     * @throws NullPointerException if <code>storage</code> is null
+     * @throws ArrayIndexOutOfBoundsException if <code>index0</code> and <code>size</code> are
+     *             inconsistent with <code>storage.length</code>
+     * @throws PyException (BufferError) when expectations do not correspond with the type
+     */
+    public SimpleNIOBuffer(int flags, BufferProtocol obj, ByteBuffer storage, int index0, int size)
+            throws PyException, ArrayIndexOutOfBoundsException, NullPointerException {
+        this(obj, storage.duplicate(), index0, size);   // Construct checked SimpleNIOBuffer
+        checkRequestFlags(flags);                       // Check request is compatible with type
+    }
+
+    /**
+     * Provide an instance of <code>SimpleNIOBuffer</code>, on the entirety of a {@link ByteBuffer},
+     * with navigation variables initialised, for sub-class use. The buffer ( {@link #storage},
+     * {@link #index0}), and the navigation ({@link #shape} array) will be initialised from the
+     * argument.
+     *
+     * @param obj exporting object (or <code>null</code>)
+     * @param storage the <code>ByteBuffer</code> wrapping the exported object state. NOTE: this
+     *            <code>PyBuffer</code> keeps a reference and may manipulate the position, mark and
+     *            limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy.
+     * @throws NullPointerException if <code>storage</code> is null
+     */
+    protected SimpleNIOBuffer(BufferProtocol obj, ByteBuffer storage) throws NullPointerException {
+        this(obj, storage, 0, storage.capacity());
+    }
+
+    /**
+     * Provide an instance of <code>SimpleNIOBuffer</code>, on the entirety of a {@link ByteBuffer},
+     * meeting the consumer's expectations as expressed in the <code>flags</code> argument, which is
+     * checked against the capabilities of the buffer type. No reference will be kept to the
+     * <code>ByteBuffer</code> passed in. (It is duplicated.)
+     *
+     * @param flags consumer requirements
+     * @param obj exporting object (or <code>null</code>)
+     * @param storage the <code>ByteBuffer</code> wrapping the exported object state
+     * @throws NullPointerException if <code>storage</code> is null
+     * @throws PyException (BufferError) when expectations do not correspond with the type
+     */
+    public SimpleNIOBuffer(int flags, BufferProtocol obj, ByteBuffer storage) throws PyException,
+            NullPointerException {
+        this(obj, storage.duplicate()); // Construct SimpleNIOBuffer on whole ByteBuffer
+        checkRequestFlags(flags);       // Check request is compatible with type
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * <code>SimpleNIOBuffer</code> provides an implementation optimised for contiguous bytes in
+     * one-dimension.
+     */
+    @Override
+    public int getLen() {
+        // Simplify for one-dimensional contiguous bytes
+        return shape[0];
+    }
+
+    @Override
+    public final int byteIndex(int index) throws IndexOutOfBoundsException {
+        return index0 + index;
+    }
+
+    // XXX Consider moving to clauses in getBufferSlice(int, int, int, int)
+    // to avoid delegation loop where that delegates to this but in BaseBuffer the reverse.
+    @Override
+    public PyBuffer getBufferSlice(int flags, int start, int count) {
+        if (count > 0) {
+            // Translate relative to underlying buffer
+            int compIndex0 = index0 + start;
+            // Create the slice from the sub-range of the buffer
+            return new SimpleView(getRoot(), flags, storage, compIndex0, count);
+        } else {
+            // Special case for count==0 where above logic would fail. Efficient too.
+            return new ZeroByteBuffer.View(getRoot(), flags);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * <code>SimpleNIOBuffer</code> provides an implementation for slicing contiguous bytes in one
+     * dimension. In that case, <i>x(i) = u(r+i)</i> for <i>i = 0..L-1</i> where u is the underlying
+     * buffer, and <i>r</i> and <i>L</i> are the start and count with which <i>x</i> was created
+     * from <i>u</i>. Thus <i>y(k) = u(r+s+km)</i>, that is, the composite offset is <i>r+s</i> and
+     * the stride is <i>m</i>.
+     */
+    @Override
+    public PyBuffer getBufferSlice(int flags, int start, int count, int stride) {
+
+        if (stride == 1 || count < 2) {
+            // Unstrided slice of simple buffer is special case
+            return getBufferSlice(flags, start, count);
+
+        } else {
+            // Translate relative to underlying buffer
+            int compIndex0 = index0 + start;
+            // Construct a view, taking a lock on the root object (this or this.root)
+            return new Strided1DNIOBuffer.SlicedView(getRoot(), flags, storage, compIndex0, count,
+                    stride);
+        }
+    }
+
+    /**
+     * A <code>SimpleNIOBuffer.SimpleView</code> represents a contiguous subsequence of another
+     * <code>SimpleNIOBuffer</code>.
+     */
+    static class SimpleView extends SimpleNIOBuffer {
+
+        /** The buffer on which this is a slice view */
+        PyBuffer root;
+
+        /**
+         * Construct a slice of a SimpleNIOBuffer.
+         *
+         * @param root buffer which will be acquired and must be released ultimately
+         * @param flags the request flags of the consumer that requested the slice
+         * @param storage <code>ByteBuffer</code> wrapping exported data (no reference kept)
+         * @param offset where the data starts in that buffer (item[0])
+         * @param count the number of items in the sliced view
+         */
+        public SimpleView(PyBuffer root, int flags, ByteBuffer storage, int offset, int count) {
+            // Create a new SimpleNIOBuffer on the buffer passed in (part of the root)
+            super(flags, root.getObj(), storage, offset, count);
+            // Get a lease on the root PyBuffer
+            this.root = root.getBuffer(FULL_RO);
+        }
+
+        @Override
+        protected PyBuffer getRoot() {
+            return root;
+        }
+    }
+}
diff --git a/src/org/python/core/buffer/SimpleStringBuffer.java b/src/org/python/core/buffer/SimpleStringBuffer.java
--- a/src/org/python/core/buffer/SimpleStringBuffer.java
+++ b/src/org/python/core/buffer/SimpleStringBuffer.java
@@ -2,17 +2,18 @@
 
 import java.nio.ByteBuffer;
 
+import org.python.core.BufferProtocol;
 import org.python.core.PyBuffer;
 import org.python.core.util.StringUtil;
 
 /**
  * Buffer API that appears to be a one-dimensional array of one-byte items providing read-only API,
  * but which is actually backed by a Java String. Some of the buffer API absolutely needs access to
- * the data as a byte array (those parts that involve a <code>PyBuffer.Pointer</code> result), and
- * therefore this class must create a byte array from the String for them. However, it defers
- * creation of a byte array until that part of the API is actually used. Where possible, this class
- * overrides those methods in SimpleBuffer that would otherwise access the byte array attribute to
- * use the String instead.
+ * the data as a byte array (those parts that involve a {@link java.nio.ByteBuffer} or
+ * {@link PyBuffer.Pointer} result), and therefore this class must create a byte array from the
+ * String for them. However, it defers creation of a byte array until that part of the API is
+ * actually used. Where possible, this class overrides those methods in SimpleBuffer that would
+ * otherwise access the byte array attribute to use the String instead.
  */
 public class SimpleStringBuffer extends SimpleBuffer {
 
@@ -26,13 +27,18 @@
      * Provide an instance of SimpleStringBuffer meeting the consumer's expectations as expressed in
      * the flags argument.
      *
+     * @param flags consumer requirements
+     * @param obj exporting object (or <code>null</code>)
      * @param bufString storing the implementation of the object
-     * @param flags consumer requirements
      */
-    public SimpleStringBuffer(int flags, String bufString) {
+    public SimpleStringBuffer(int flags, BufferProtocol obj, String bufString) {
+        /*
+         * Leaving storage=null is ok because we carefully override every method that uses it,
+         * deferring creation of the storage byte array until we absolutely must have one.
+         */
+        super(obj, null, 0, bufString.length());
         // Save the backing string
         this.bufString = bufString;
-        shape[0] = bufString.length();
         // Check request is compatible with type
         checkRequestFlags(flags);
     }
@@ -54,32 +60,31 @@
      * This method uses {@link String#charAt(int)} rather than create an actual byte buffer.
      */
     @Override
-    public byte byteAt(int index) throws IndexOutOfBoundsException {
-        // Avoid creating buf by using String.charAt
+    public final byte byteAtImpl(int index) {
         return (byte)bufString.charAt(index);
     }
 
     /**
      * {@inheritDoc}
      * <p>
+     * In <code>SimpleStringBuffer</code> we can simply return the argument.
+     */
+    @Override
+    public final int byteIndex(int index) {
+        // We do not check the index because String will do it for us.
+        return index;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
      * This method uses {@link String#charAt(int)} rather than create an actual byte buffer.
      */
     @Override
-    public int intAt(int index) throws IndexOutOfBoundsException {
-        // Avoid creating buf by using String.charAt
-        return bufString.charAt(index);
-    }
-
-    /**
-     * {@inheritDoc}
-     * <p>
-     * This method uses {@link String#charAt(int)} rather than create an actual byte buffer.
-     */
-    @Override
-    public void copyTo(int srcIndex, byte[] dest, int destPos, int length)
+    public void copyTo(int srcIndex, byte[] dest, int destPos, int count)
             throws IndexOutOfBoundsException {
         // Avoid creating buf by using String.charAt
-        int endIndex = srcIndex + length, p = destPos;
+        int endIndex = srcIndex + count, p = destPos;
         for (int i = srcIndex; i < endIndex; i++) {
             dest[p++] = (byte)bufString.charAt(i);
         }
@@ -91,13 +96,12 @@
      * The <code>SimpleStringBuffer</code> implementation avoids creation of a byte buffer.
      */
     @Override
-    public PyBuffer getBufferSlice(int flags, int start, int length) {
-        if (length > 0) {
-            // The new string content is just a sub-string. (Non-copy operation in Java.)
-            return new SimpleStringView(getRoot(), flags,
-                    bufString.substring(start, start + length));
+    public PyBuffer getBufferSlice(int flags, int start, int count) {
+        if (count > 0) {
+            // The new string content is just a sub-string.
+            return new SimpleStringView(getRoot(), flags, bufString.substring(start, start + count));
         } else {
-            // Special case for length==0 where start out of bounds sometimes raises exception.
+            // Special case for count==0 where start out of bounds sometimes raises exception.
             return new ZeroByteBuffer.View(getRoot(), flags);
         }
     }
@@ -108,23 +112,26 @@
      * The <code>SimpleStringBuffer</code> implementation creates an actual byte buffer.
      */
     @Override
-    public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {
+    public PyBuffer getBufferSlice(int flags, int start, int count, int stride) {
         if (stride == 1) {
             // Unstrided slice of a SimpleStringBuffer is itself a SimpleStringBuffer.
-            return getBufferSlice(flags, start, length);
+            return getBufferSlice(flags, start, count);
         } else {
             // Force creation of the actual byte array from the String.
             ensureHaveBytes();
             // Now we are effectively a SimpleBuffer, return the strided view.
-            return super.getBufferSlice(flags, start, length, stride);
+            return super.getBufferSlice(flags, start, count, stride);
         }
     }
 
     @Override
-    public ByteBuffer getNIOByteBuffer() {
+    protected ByteBuffer getNIOByteBufferImpl() {
         // Force creation of the actual byte array from the String.
         ensureHaveBytes();
-        return super.getNIOByteBuffer().asReadOnlyBuffer();
+        // The buffer spans the whole storage, which may include data not in the view
+        ByteBuffer b = ByteBuffer.wrap(storage);
+        // Return as read-only.
+        return b.asReadOnlyBuffer();
     }
 
     /**
@@ -142,6 +149,7 @@
      * <p>
      * This method creates an actual byte array from the underlying String if none yet exists.
      */
+    @SuppressWarnings("deprecation")
     @Override
     public Pointer getBuf() {
         ensureHaveBytes();
@@ -153,6 +161,7 @@
      * <p>
      * This method creates an actual byte array from the underlying String if none yet exists.
      */
+    @SuppressWarnings("deprecation")
     @Override
     public Pointer getPointer(int index) {
         ensureHaveBytes();
@@ -164,6 +173,7 @@
      * <p>
      * This method creates an actual byte array from the underlying String if none yet exists.
      */
+    @SuppressWarnings("deprecation")
     @Override
     public Pointer getPointer(int... indices) {
         ensureHaveBytes();
@@ -197,7 +207,7 @@
          */
         public SimpleStringView(PyBuffer root, int flags, String bufString) {
             // Create a new SimpleStringBuffer on the string passed in
-            super(flags, bufString);
+            super(flags, root.getObj(), bufString);
             // Get a lease on the root PyBuffer
             this.root = root.getBuffer(FULL_RO);
         }
@@ -206,12 +216,5 @@
         protected PyBuffer getRoot() {
             return root;
         }
-
-        @Override
-        public void releaseAction() {
-            // We have to release the root too if ours was final.
-            root.release();
-        }
-
     }
 }
diff --git a/src/org/python/core/buffer/SimpleWritableBuffer.java b/src/org/python/core/buffer/SimpleWritableBuffer.java
--- a/src/org/python/core/buffer/SimpleWritableBuffer.java
+++ b/src/org/python/core/buffer/SimpleWritableBuffer.java
@@ -1,5 +1,6 @@
 package org.python.core.buffer;
 
+import org.python.core.BufferProtocol;
 import org.python.core.PyBuffer;
 import org.python.core.PyException;
 
@@ -14,16 +15,17 @@
      * against the capabilities of the buffer type.
      *
      * @param flags consumer requirements
+     * @param obj exporting object (or <code>null</code>)
      * @param storage the array of bytes storing the implementation of the exporting object
      * @param index0 offset where the data starts in that array (item[0])
      * @param size the number of bytes occupied
      * @throws PyException (BufferError) when expectations do not correspond with the type
      */
-    public SimpleWritableBuffer(int flags, byte[] storage, int index0, int size)
+    public SimpleWritableBuffer(int flags, BufferProtocol obj, byte[] storage, int index0, int size)
             throws PyException, NullPointerException {
-        super(storage, index0, size);   // Construct checked SimpleBuffer
+        super(obj, storage, index0, size);      // Construct checked SimpleBuffer
         addFeatureFlags(WRITABLE);
-        checkRequestFlags(flags);       // Check request is compatible with type
+        checkRequestFlags(flags);               // Check request is compatible with type
     }
 
     /**
@@ -32,68 +34,34 @@
      * checked against the capabilities of the buffer type.
      *
      * @param flags consumer requirements
+     * @param obj exporting object (or <code>null</code>)
      * @param storage the array of bytes storing the implementation of the exporting object
      * @throws PyException (BufferError) when expectations do not correspond with the type
      */
-    public SimpleWritableBuffer(int flags, byte[] storage) throws PyException, NullPointerException {
-        super(storage);                 // Construct SimpleBuffer on whole array
-        addFeatureFlags(WRITABLE);
-        checkRequestFlags(flags);       // Check request is compatible with type
-    }
-
-    @Override
-    public boolean isReadonly() {
-        return false;
+    public SimpleWritableBuffer(int flags, BufferProtocol obj, byte[] storage) throws PyException,
+            NullPointerException {
+        this(flags, obj, storage, 0, storage.length);
     }
 
     /**
      * {@inheritDoc}
      * <p>
-     * <code>SimpleBuffer</code> provides an implementation optimised for contiguous bytes in
-     * one-dimension.
+     * Declared <code>final</code> returning <code>true</code> in <code>SimpleWritableBuffer</code>
+     * to make checks unnecessary.
      */
     @Override
-    public void storeAt(byte value, int index) {
-        // Implement directly and don't ask whether read-only
-        storage[index0 + index] = value;
+    public final boolean isReadonly() {
+        return false;
     }
 
-    /**
-     * {@inheritDoc}
-     * <p>
-     * <code>SimpleBuffer</code> provides an implementation optimised for contiguous bytes in
-     * one-dimension.
-     */
+    /** Do nothing: the buffer is writable. */
     @Override
-    public void storeAt(byte value, int... indices) {
-        checkDimension(indices.length);
-        storeAt(value, indices[0]);
-    }
+    protected final void checkWritable() {}
 
-    /**
-     * {@inheritDoc}
-     * <p>
-     * <code>SimpleBuffer</code> provides an implementation optimised for contiguous bytes in
-     * one-dimension.
-     */
     @Override
-    public void copyFrom(byte[] src, int srcPos, int destIndex, int length) {
-        System.arraycopy(src, srcPos, storage, index0 + destIndex, length);
-    }
-
-    /**
-     * {@inheritDoc}
-     * <p>
-     * <code>SimpleBuffer</code> provides an implementation optimised for contiguous bytes in
-     * one-dimension.
-     */
-    @Override
-    public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException {
-        if (src.getLen() != getLen()) {
-            throw differentStructure();
-        }
-        // Get the source to deliver efficiently to our byte storage
-        src.copyTo(storage, index0);
+    protected void storeAtImpl(byte value, int byteIndex) {
+        // Implement directly and don't ask whether read-only
+        storage[byteIndex] = value;
     }
 
     /**
@@ -103,14 +71,14 @@
      * writable.
      */
     @Override
-    public PyBuffer getBufferSlice(int flags, int start, int length) {
-        if (length > 0) {
+    public PyBuffer getBufferSlice(int flags, int start, int count) {
+        if (count > 0) {
             // Translate relative to underlying buffer
             int compIndex0 = index0 + start;
             // Create the slice from the sub-range of the buffer
-            return new SimpleView(getRoot(), flags, storage, compIndex0, length);
+            return new SimpleView(getRoot(), flags, storage, compIndex0, count);
         } else {
-            // Special case for length==0 where above logic would fail. Efficient too.
+            // Special case for count==0 where above logic would fail. Efficient too.
             return new ZeroByteBuffer.View(getRoot(), flags);
         }
     }
@@ -122,18 +90,18 @@
      * writable.
      */
     @Override
-    public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {
+    public PyBuffer getBufferSlice(int flags, int start, int count, int stride) {
 
-        if (stride == 1 || length < 2) {
+        if (stride == 1 || count < 2) {
             // Unstrided slice of simple buffer is itself simple
-            return getBufferSlice(flags, start, length);
+            return getBufferSlice(flags, start, count);
 
         } else {
             // Translate relative to underlying buffer
             int compIndex0 = index0 + start;
             // Construct a view, taking a lock on the root object (this or this.root)
             return new Strided1DWritableBuffer.SlicedView(getRoot(), flags, storage, compIndex0,
-                    length, stride);
+                    count, stride);
         }
     }
 
@@ -157,7 +125,7 @@
          */
         public SimpleView(PyBuffer root, int flags, byte[] storage, int index0, int size) {
             // Create a new SimpleBuffer on the buffer passed in (part of the root)
-            super(flags, storage, index0, size);
+            super(flags, root.getObj(), storage, index0, size);
             // Get a lease on the root PyBuffer
             this.root = root.getBuffer(FULL_RO);
         }
@@ -166,13 +134,5 @@
         protected PyBuffer getRoot() {
             return root;
         }
-
-        @Override
-        public void releaseAction() {
-            // We have to release the root too if ours was final.
-            root.release();
-        }
-
     }
-
 }
diff --git a/src/org/python/core/buffer/Strided1DBuffer.java b/src/org/python/core/buffer/Strided1DBuffer.java
--- a/src/org/python/core/buffer/Strided1DBuffer.java
+++ b/src/org/python/core/buffer/Strided1DBuffer.java
@@ -1,11 +1,12 @@
 package org.python.core.buffer;
 
+import org.python.core.BufferProtocol;
 import org.python.core.PyBuffer;
 import org.python.core.PyException;
 
 /**
  * Read-only buffer API over a one-dimensional array of one-byte items, that are evenly-spaced in a
- * storage array. The buffer has <code>storage</code>, <code>index0</code> and <code>length</code>
+ * storage array. The buffer has <code>storage</code>, <code>index0</code> and <code>count</code>
  * properties in the usual way, designating a slice (or all) of a byte array, but also a
  * <code>stride</code> property (equal to <code>getStrides()[0]</code>).
  * <p>
@@ -14,7 +15,7 @@
  * Designate by <i>x(j)</i>, for <i>j=0..L-1</i>, the byte at index <i>j</i>, that is, the byte
  * retrieved by <code>x.byteAt(j)</code>. Thus, we store <i>x(j)</i> at <i>u(a+pj)</i>, that is,
  * <i>x(0) = u(a)</i>. When we construct such a buffer, we have to supply <i>a</i> =
- * <code>index0</code>, <i>L</i> = <code>length</code>, and <i>p</i> = <code>stride</code> as the
+ * <code>index0</code>, <i>L</i> = <code>count</code>, and <i>p</i> = <code>stride</code> as the
  * constructor arguments. The last item in the slice <i>x(L-1)</i> is stored at <i>u(a+p(L-1))</i>.
  * For the simple case of positive stride, constructor argument <code>index0</code> is the low index
  * of the range occupied by the data. When the stride is negative, that is to say <i>p<0</i>, and
@@ -28,7 +29,7 @@
  * create the <code>memoryview</code> that is returned as an extended slice of a
  * <code>memoryview</code>.
  */
-public class Strided1DBuffer extends BaseBuffer {
+public class Strided1DBuffer extends BaseArrayBuffer {
 
     /**
      * Step size in the underlying buffer essential to correct translation of an index (or indices)
@@ -38,30 +39,6 @@
     protected int stride;
 
     /**
-     * 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 the navigational properties and call {@link #checkRequestFlags(int)} passing the
-     * consumer's request flags.
-     *
-     * <pre>
-     * this.storage = storage;          // Exported data
-     * this.index0 = index0;            // Index to be treated as item[0]
-     * this.shape[0] = length;          // Number of items in exported data
-     * this.stride = stride;            // Between items
-     * </pre>
-     *
-     * The pre-defined {@link #strides} field remains <code>null</code> until {@link #getStrides} is
-     * called.
-     */
-    protected Strided1DBuffer() {
-        super(STRIDES);
-        // Initialise navigation
-        shape = new int[1];
-        // strides is created on demand;
-        // suboffsets is always null for this type.
-    }
-
-    /**
      * Provide an instance of <code>Strided1DBuffer</code> with navigation variables initialised,
      * for sub-class use. The buffer ({@link #storage}, {@link #index0}), and the navigation (
      * {@link #shape} array and {@link #stride}) will be initialised from the arguments (which are
@@ -69,27 +46,25 @@
      * <p>
      * The sub-class constructor should check that the intended access is compatible with this
      * object by calling {@link #checkRequestFlags(int)}. (See the source of
-     * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, byte[], int, int, int)}
-     * for an example of this use.)
+     * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, byte[], int, int, int)} for an
+     * example of this use.)
      *
+     * @param obj exporting object (or <code>null</code>)
      * @param storage raw byte array containing exported data
      * @param index0 index into storage of item[0]
-     * @param length number of items in the slice
+     * @param count number of items in the slice
      * @param stride in between successive elements of the new PyBuffer
      * @throws NullPointerException if <code>storage</code> is null
-     * @throws ArrayIndexOutOfBoundsException if <code>index0</code>, <code>length</code> and
+     * @throws ArrayIndexOutOfBoundsException if <code>index0</code>, <code>count</code> and
      *             <code>stride</code> are inconsistent with <code>storage.length</code>
      */
-    // XXX: "for sub-class use" = should be protected?
-    public Strided1DBuffer(byte[] storage, int index0, int length, int stride)
+    protected Strided1DBuffer(BufferProtocol obj, byte[] storage, int index0, int count, int stride)
             throws ArrayIndexOutOfBoundsException, NullPointerException {
-        this();
-        this.storage = storage;         // Exported data
-        this.index0 = index0;           // Index to be treated as item[0]
-        this.shape[0] = length;         // Number of items in exported data
+        super(storage, STRIDES, index0, count, stride);
+        this.obj = obj;
         this.stride = stride;           // Between items
 
-        if (length == 0) {
+        if (count == 0) {
             // Nothing to check as we'll make no accesses
             addFeatureFlags(CONTIGUITY);
 
@@ -99,20 +74,20 @@
 
             if (stride == 1) {
                 lo = index0;                                // First byte of item[0]
-                hi = index0 + length;                       // Last byte of item[L-1] + 1
+                hi = index0 + count;                        // Last byte of item[L-1] + 1
                 addFeatureFlags(CONTIGUITY);
 
             } else if (stride > 1) {
                 lo = index0;                                // First byte of item[0]
-                hi = index0 + (length - 1) * stride + 1;    // Last byte of item[L-1] + 1
+                hi = index0 + (count - 1) * stride + 1;     // Last byte of item[L-1] + 1
 
             } else {
                 hi = index0 + 1;                            // Last byte of item[0] + 1
-                lo = index0 + (length - 1) * stride;        // First byte of item[L-1]
+                lo = index0 + (count - 1) * stride;         // First byte of item[L-1]
             }
 
             // Check indices using "all non-negative" trick
-            if ((length | lo | (storage.length - lo) | hi | (storage.length - hi)) < 0) {
+            if ((count | lo | (storage.length - lo) | hi | (storage.length - hi)) < 0) {
                 throw new ArrayIndexOutOfBoundsException();
             }
         }
@@ -122,31 +97,32 @@
      * Provide an instance of <code>Strided1DBuffer</code> on a particular array of bytes specifying
      * a starting index, the number of items in the result, and a byte-indexing stride. The result
      * of <code>byteAt(i)</code> will be equal to <code>storage[index0+stride*i]</code> (whatever
-     * the sign of <code>stride</code>), valid for <code>0<=i<length</code>. The constructor
+     * the sign of <code>stride</code>), valid for <code>0<=i<count</code>. The constructor
      * checks that all these indices lie within the <code>storage</code> array (unless
-     * <code>length=0</code>).
+     * <code>count=0</code>).
      * <p>
      * The constructed <code>PyBuffer</code> meets the consumer's expectations as expressed in the
      * <code>flags</code> argument, or an exception will be thrown if these are incompatible with
      * the type (e.g. the consumer does not specify that it understands the strides array). Note
      * that the actual range in the <code>storage</code> array, the lowest and highest index, is not
-     * explicitly passed, but is implicit in <code>index0</code>, <code>length</code> and
+     * explicitly passed, but is implicit in <code>index0</code>, <code>count</code> and
      * <code>stride</code>. The constructor checks that these indices lie within the
-     * <code>storage</code> array (unless <code>length=0</code>).
+     * <code>storage</code> array (unless <code>count=0</code>).
      *
      * @param flags consumer requirements
+     * @param obj exporting object (or <code>null</code>)
      * @param storage raw byte array containing exported data
      * @param index0 index into storage of item[0]
-     * @param length number of items in the slice
-     * @param stride in between successive elements of the new PyBuffer
+     * @param count number of items in the slice
+     * @param stride byte-index distance from one element to the next in the new PyBuffer
      * @throws NullPointerException if <code>storage</code> is null
-     * @throws ArrayIndexOutOfBoundsException if <code>index0</code>, <code>length</code> and
+     * @throws ArrayIndexOutOfBoundsException if <code>index0</code>, <code>count</code> and
      *             <code>stride</code> are inconsistent with <code>storage.length</code>
      * @throws PyException (BufferError) when expectations do not correspond with the type
      */
-    public Strided1DBuffer(int flags, byte[] storage, int index0, int length, int stride)
+    public Strided1DBuffer(int flags, BufferProtocol obj, byte[] storage, int index0, int count, int stride)
             throws ArrayIndexOutOfBoundsException, NullPointerException, PyException {
-        this(storage, index0, length, stride);
+        this(obj, storage, index0, count, stride);
         checkRequestFlags(flags);   // Check request is compatible with type
 
     }
@@ -157,78 +133,46 @@
     }
 
     @Override
-    public byte byteAt(int index) throws IndexOutOfBoundsException {
-        return storage[index0 + index * stride];
-    }
-
-    @Override
-    protected int calcIndex(int index) throws IndexOutOfBoundsException {
+    public final int byteIndex(int index) throws IndexOutOfBoundsException {
+        if (index < 0 || index >= shape[0]) {
+            throw new IndexOutOfBoundsException();
+        }
         return index0 + index * stride;
     }
 
-    @Override
-    protected int calcIndex(int... indices) throws IndexOutOfBoundsException {
-        // BaseBuffer implementation can be simplified since if indices.length!=1 we error.
-        checkDimension(indices.length); // throws if != 1
-        return calcIndex(indices[0]);
-    }
-
-    /**
-     * {@inheritDoc} <code>Strided1DBuffer</code> provides a version optimised for strided bytes in
-     * one dimension.
-     */
-    @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 d = destPos;
-
-        // Strategy depends on whether items are laid end-to-end contiguously or there are gaps
-        if (stride == 1) {
-            // stride == itemsize: straight copy of contiguous bytes
-            System.arraycopy(storage, s, dest, d, length);
-
-        } else {
-            // Non-contiguous copy: single byte items
-            int limit = s + length * stride;
-            for (; s != limit; s += stride) {
-                dest[d++] = storage[s];
-            }
-        }
-    }
-
     /**
      * {@inheritDoc}
      * <p>
      * <code>Strided1DBuffer</code> provides an implementation for slicing already-strided bytes in
      * one dimension. In that case, <i>x(i) = u(r+ip)</i> for <i>i = 0..L-1</i> where u is the
-     * underlying buffer, and <i>r</i>, <i>p</i> and <i>L</i> are the start, stride and length with
+     * underlying buffer, and <i>r</i>, <i>p</i> and <i>L</i> are the start, stride and count with
      * which <i>x</i> was created from <i>u</i>. Thus <i>y(k) = u(r+sp+kmp)</i>, that is, the
      * composite <code>index0</code> is <i>r+sp</i> and the composite <code>stride</code> is
      * <i>mp</i>.
      */
     @Override
-    public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {
+    public PyBuffer getBufferSlice(int flags, int start, int count, int stride) {
 
-        if (length > 0) {
+        if (count > 0) {
             // Translate start relative to underlying buffer
             int compStride = this.stride * stride;
             int compIndex0 = index0 + start * this.stride;
             // Construct a view, taking a lock on the root object (this or this.root)
-            return new SlicedView(getRoot(), flags, storage, compIndex0, length, compStride);
+            return new SlicedView(getRoot(), flags, storage, compIndex0, count, compStride);
 
         } else {
-            // Special case for length==0 where above logic would fail. Efficient too.
+            // Special case for count==0 where above logic would fail. Efficient too.
             return new ZeroByteBuffer.View(getRoot(), flags);
         }
     }
 
+    @SuppressWarnings("deprecation")
     @Override
     public Pointer getPointer(int index) {
         return new Pointer(storage, index0 + index * stride);
     }
 
+    @SuppressWarnings("deprecation")
     @Override
     public Pointer getPointer(int... indices) {
         // BaseBuffer implementation can be simplified since if indices.length!=1 we error.
@@ -236,15 +180,6 @@
         return getPointer(indices[0]);
     }
 
-    @Override
-    public int[] getStrides() {
-        if (strides == null) {
-            strides = new int[1];
-            strides[0] = stride;
-        }
-        return strides;
-    }
-
     /**
      * A <code>Strided1DBuffer.SlicedView</code> represents a non-contiguous subsequence of a simple
      * buffer.
@@ -261,14 +196,14 @@
          * @param flags consumer requirements
          * @param storage raw byte array containing exported data
          * @param index0 index into storage of item[0]
-         * @param len number of items in the slice
+         * @param count number of items in the sliced view
          * @param stride in between successive elements of the new PyBuffer
          * @throws PyException (BufferError) when expectations do not correspond with the type
          */
-        public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int len, int stride)
-                throws PyException {
+        public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int count,
+                int stride) throws PyException {
             // Create a new on the buffer passed in (part of the root)
-            super(flags, storage, index0, len, stride);
+            super(flags, root.getObj(), storage, index0, count, stride);
             // Get a lease on the root PyBuffer (read-only)
             this.root = root.getBuffer(FULL_RO);
         }
@@ -277,13 +212,5 @@
         protected PyBuffer getRoot() {
             return root;
         }
-
-        @Override
-        public void releaseAction() {
-            // We have to release the root too if ours was final.
-            root.release();
-        }
-
     }
-
 }
diff --git a/src/org/python/core/buffer/Strided1DNIOBuffer.java b/src/org/python/core/buffer/Strided1DNIOBuffer.java
new file mode 100644
--- /dev/null
+++ b/src/org/python/core/buffer/Strided1DNIOBuffer.java
@@ -0,0 +1,209 @@
+package org.python.core.buffer;
+
+import java.nio.ByteBuffer;
+
+import org.python.core.BufferProtocol;
+import org.python.core.PyBuffer;
+import org.python.core.PyException;
+
+/**
+ * Read-only buffer API over a one-dimensional <code>java.nio.ByteBuffer</code> of one-byte items,
+ * that are evenly-spaced in that store. The buffer has <code>storage</code>, <code>index0</code>
+ * and <code>length</code> properties in the usual way, designating a slice (or all) of a byte
+ * array, but also a <code>stride</code> property (equal to <code>getStrides()[0]</code>).
+ * <p>
+ * Let the underlying buffer be the byte sequence <i>u(i)</i> for <i>i=0..N-1</i>, let <i>x</i> be
+ * the <code>Strided1DNIOBuffer</code>, and let the stride be <i>p</i>. The storage works as
+ * follows. Designate by <i>x(j)</i>, for <i>j=0..L-1</i>, the byte at index <i>j</i>, that is, the
+ * byte retrieved by <code>x.byteAt(j)</code>. Thus, we store <i>x(j)</i> at <i>u(a+pj)</i>, that
+ * is, <i>x(0) = u(a)</i>. When we construct such a buffer, we have to supply <i>a</i> =
+ * <code>index0</code>, <i>L</i> = <code>count</code>, and <i>p</i> = <code>stride</code> as the
+ * constructor arguments. The last item in the slice <i>x(L-1)</i> is stored at <i>u(a+p(L-1))</i>.
+ * For the simple case of positive stride, constructor argument <code>index0</code> is the low index
+ * of the range occupied by the data. When the stride is negative, that is to say <i>p<0</i>, and
+ * <i>L>1</i>, this will be to the left of <i>u(a)</i>, and the constructor argument
+ * <code>index0</code> is not then the low index of the range occupied by the data. Clearly both
+ * these indexes must be in the range 0 to <i>N-1</i> inclusive, a rule enforced by the constructors
+ * (unless <i>L=0</i>, when it is assumed no array access will take place).
+ * <p>
+ * The class may be used by exporters to create a strided slice (e.g. to export the diagonal of a
+ * matrix) and in particular by other buffers to create strided slices of themselves, such as to
+ * create the <code>memoryview</code> that is returned as an extended slice of a
+ * <code>memoryview</code>.
+ */
+public class Strided1DNIOBuffer extends BaseNIOBuffer {
+
+    /**
+     * Step size in the underlying buffer essential to correct translation of an index (or indices)
+     * into an index into the storage. The value is returned by {@link #getStrides()} is an array
+     * with this as the only element.
+     */
+    protected int stride;
+
+    /**
+     * Provide an instance of <code>Strided1DNIOBuffer</code> with navigation variables initialised,
+     * for sub-class use. The buffer ({@link #storage}, {@link #index0}), and the navigation (
+     * {@link #shape} array and {@link #stride}) will be initialised from the arguments (which are
+     * checked for range).
+     * <p>
+     * The sub-class constructor should check that the intended access is compatible with this
+     * object by calling {@link #checkRequestFlags(int)}. (See the source of
+     * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, ByteBuffer, int, int, int)} for
+     * an example of this use.)
+     *
+     * @param obj exporting object (or <code>null</code>)
+     * @param storage the <code>ByteBuffer</code> wrapping the exported object state. NOTE: this
+     *            <code>PyBuffer</code> keeps a reference and may manipulate the position, mark and
+     *            limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy.
+     * @param index0 index into storage of item[0]
+     * @param count number of items in the slice
+     * @param stride in between successive elements of the new PyBuffer
+     * @throws NullPointerException if <code>storage</code> is null
+     * @throws ArrayIndexOutOfBoundsException if <code>index0</code>, <code>count</code> and
+     *             <code>stride</code> are inconsistent with <code>storage.length</code>
+     */
+    protected Strided1DNIOBuffer(BufferProtocol obj, ByteBuffer storage, int index0, int count,
+            int stride) throws ArrayIndexOutOfBoundsException, NullPointerException {
+        super(storage, STRIDES, index0, count, stride);
+        this.obj = obj;
+        this.stride = stride;           // Between items
+
+        if (count == 0) {
+            // Nothing to check as we'll make no accesses
+            addFeatureFlags(CONTIGUITY);
+
+        } else {
+            // Need to check lowest and highest index against array
+            int lo, hi;
+
+            if (stride == 1) {
+                lo = index0;                                // First byte of item[0]
+                hi = index0 + count;                        // Last byte of item[L-1] + 1
+                addFeatureFlags(CONTIGUITY);
+
+            } else if (stride > 1) {
+                lo = index0;                                // First byte of item[0]
+                hi = index0 + (count - 1) * stride + 1;     // Last byte of item[L-1] + 1
+
+            } else {
+                hi = index0 + 1;                            // Last byte of item[0] + 1
+                lo = index0 + (count - 1) * stride;         // First byte of item[L-1]
+            }
+
+            // Check indices using "all non-negative" trick
+            int cap = storage.capacity();
+            if ((count | lo | (cap - lo) | hi | (cap - hi)) < 0) {
+                throw new ArrayIndexOutOfBoundsException();
+            }
+        }
+
+        // Deduce feature flags from the client's ByteBuffer
+        if (!storage.isReadOnly()) {
+            addFeatureFlags(WRITABLE);
+        }
+        if (storage.hasArray()) {
+            addFeatureFlags(AS_ARRAY);
+        }
+    }
+
+    /**
+     * Provide an instance of <code>Strided1DNIOBuffer</code> on a particular {@link ByteBuffer}
+     * specifying a starting index, the number of items in the result, and a byte-indexing stride.
+     * The result of <code>byteAt(i)</code> will be equal to
+     * <code>storage.get(index0+stride*i)</code> (whatever the sign of <code>stride</code>), valid
+     * for <code>0<=i<count</code>. The constructor checks that all these indices lie within
+     * the <code>storage</code> (unless <code>count=0</code>). No reference will be kept to the
+     * <code>ByteBuffer</code> passed in. (It is duplicated.)
+     * <p>
+     * The constructed <code>PyBuffer</code> meets the consumer's expectations as expressed in the
+     * <code>flags</code> argument, or an exception will be thrown if these are incompatible with
+     * the type (e.g. the consumer does not specify that it understands the strides array). Note
+     * that the actual range in the <code>storage</code> array, the lowest and highest index, is not
+     * explicitly passed, but is implicit in <code>index0</code>, <code>count</code> and
+     * <code>stride</code>. The constructor checks that these indices lie within the
+     * <code>storage</code> array (unless <code>count=0</code>).
+     *
+     * @param flags consumer requirements
+     * @param obj exporting object (or <code>null</code>)
+     * @param storage <code>ByteBuffer</code> wrapping exported data
+     * @param index0 index into storage of item[0]
+     * @param count number of items in the slice
+     * @param stride in between successive elements of the new PyBuffer
+     * @throws NullPointerException if <code>storage</code> is null
+     * @throws ArrayIndexOutOfBoundsException if <code>index0</code>, <code>count</code> and
+     *             <code>stride</code> are inconsistent with <code>storage.length</code>
+     * @throws PyException (BufferError) when expectations do not correspond with the type
+     */
+    public Strided1DNIOBuffer(int flags, BufferProtocol obj, ByteBuffer storage, int index0,
+            int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException,
+            PyException {
+        this(obj, storage.duplicate(), index0, count, stride);
+        checkRequestFlags(flags);   // Check request is compatible with type
+
+    }
+
+    @Override
+    public final int byteIndex(int index) throws IndexOutOfBoundsException {
+        return index0 + index * stride;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * <code>Strided1DNIOBuffer</code> provides an implementation for slicing already-strided bytes
+     * in one dimension. In that case, <i>x(i) = u(r+ip)</i> for <i>i = 0..L-1</i> where u is the
+     * underlying buffer, and <i>r</i>, <i>p</i> and <i>L</i> are the start, stride and count with
+     * which <i>x</i> was created from <i>u</i>. Thus <i>y(k) = u(r+sp+kmp)</i>, that is, the
+     * composite <code>index0</code> is <i>r+sp</i> and the composite <code>stride</code> is
+     * <i>mp</i>.
+     */
+    @Override
+    public PyBuffer getBufferSlice(int flags, int start, int count, int stride) {
+
+        if (count > 0) {
+            // Translate start relative to underlying buffer
+            int compStride = this.stride * stride;
+            int compIndex0 = index0 + start * this.stride;
+            // Construct a view, taking a lock on the root object (this or this.root)
+            return new SlicedView(getRoot(), flags, storage, compIndex0, count, compStride);
+
+        } else {
+            // Special case for count==0 where above logic would fail. Efficient too.
+            return new ZeroByteBuffer.View(getRoot(), flags);
+        }
+    }
+
+    /**
+     * A <code>Strided1DNIOBuffer.SlicedView</code> represents a non-contiguous subsequence of a
+     * simple buffer.
+     */
+    static class SlicedView extends Strided1DNIOBuffer {
+
+        /** The buffer on which this is a slice view */
+        PyBuffer root;
+
+        /**
+         * Construct a slice of a one-dimensional byte buffer.
+         *
+         * @param root on which release must be called when this is released
+         * @param flags consumer requirements
+         * @param storage <code>ByteBuffer</code> wrapping exported data (no reference kept)
+         * @param index0 index into storage of item[0]
+         * @param count the number of items in the sliced view
+         * @param stride in between successive elements of the new PyBuffer
+         * @throws PyException (BufferError) when expectations do not correspond with the type
+         */
+        public SlicedView(PyBuffer root, int flags, ByteBuffer storage, int index0, int count,
+                int stride) throws PyException {
+            // Create a new slice on the buffer passed in (part of the root)
+            super(flags, root.getObj(), storage, index0, count, stride);
+            // Get a lease on the root PyBuffer (read-only)
+            this.root = root.getBuffer(FULL_RO);
+        }
+
+        @Override
+        protected PyBuffer getRoot() {
+            return root;
+        }
+    }
+}
diff --git a/src/org/python/core/buffer/Strided1DWritableBuffer.java b/src/org/python/core/buffer/Strided1DWritableBuffer.java
--- a/src/org/python/core/buffer/Strided1DWritableBuffer.java
+++ b/src/org/python/core/buffer/Strided1DWritableBuffer.java
@@ -1,5 +1,6 @@
 package org.python.core.buffer;
 
+import org.python.core.BufferProtocol;
 import org.python.core.PyBuffer;
 import org.python.core.PyException;
 
@@ -14,67 +15,54 @@
      * Provide an instance of <code>Strided1DWritableBuffer</code> on a particular array of bytes
      * specifying a starting index, the number of items in the result, and a byte-indexing stride.
      * The result of <code>byteAt(i)</code> will be equal to <code>storage[index0+stride*i]</code>
-     * (whatever the sign of <code>stride>0</code>), valid for <code>0<=i<length</code>.
+     * (whatever the sign of <code>stride>0</code>), valid for <code>0<=i<count</code>.
      * <p>
      * The constructed <code>PyBuffer</code> meets the consumer's expectations as expressed in the
      * <code>flags</code> argument, or an exception will be thrown if these are incompatible with
      * the type (e.g. the consumer does not specify that it understands the strides array). Note
      * that the actual range in the <code>storage</code> array, the lowest and highest index, is not
-     * explicitly passed, but is implicit in <code>index0</code>, <code>length</code> and
+     * explicitly passed, but is implicit in <code>index0</code>, <code>count</code> and
      * <code>stride</code>. The caller is responsible for checking these fall within the array, or
      * the sub-range the caller is allowed to use.
      *
      * @param flags consumer requirements
+     * @param obj exporting object (or <code>null</code>)
      * @param storage raw byte array containing exported data
      * @param index0 index into storage of item[0]
-     * @param length number of items in the slice
-     * @param stride in between successive elements of the new PyBuffer
+     * @param count number of items in the slice
+     * @param stride byte-index distance from one element to the next in the new PyBuffer
      * @throws NullPointerException if <code>storage</code> is null
-     * @throws ArrayIndexOutOfBoundsException if <code>index0</code>, <code>length</code> and
+     * @throws ArrayIndexOutOfBoundsException if <code>index0</code>, <code>count</code> and
      *             <code>stride</code> are inconsistent with <code>storage.length</code>
      * @throws PyException (BufferError) when expectations do not correspond with the type
      */
-    public Strided1DWritableBuffer(int flags, byte[] storage, int index0, int length, int stride)
-            throws ArrayIndexOutOfBoundsException, NullPointerException, PyException {
-        super(storage, index0, length, stride);
+    public Strided1DWritableBuffer(int flags, BufferProtocol obj, byte[] storage, int index0,
+            int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException,
+            PyException {
+        super(obj, storage, index0, count, stride);
         addFeatureFlags(WRITABLE);
         checkRequestFlags(flags);   // Check request is compatible with type
     }
 
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Declared <code>final</code> returning <code>true</code> in
+     * <code>Strided1DWritableBuffer</code> to make checks unnecessary.
+     */
     @Override
-    public boolean isReadonly() {
+    public final boolean isReadonly() {
         return false;
     }
 
+    /** Do nothing: the buffer is writable. */
     @Override
-    public void storeAt(byte value, int index) throws IndexOutOfBoundsException, PyException {
-        storage[index0 + index * stride] = value;
-    }
+    protected final void checkWritable() {}
 
-    /**
-     * {@inheritDoc} <code>Strided1DWritableBuffer</code> provides a version optimised for strided
-     * bytes in one dimension.
-     */
     @Override
-    public void copyFrom(byte[] src, int srcPos, int destIndex, int length)
-            throws IndexOutOfBoundsException, PyException {
-
-        // Data is here in the buffers
-        int s = srcPos;
-        int d = index0 + destIndex * stride;
-
-        // Strategy depends on whether items are laid end-to-end or there are gaps
-        if (stride == 1) {
-            // Straight copy of contiguous bytes
-            System.arraycopy(src, srcPos, storage, d, length);
-
-        } else {
-            // Non-contiguous copy: single byte items
-            int limit = d + length * stride;
-            for (; d != limit; d += stride) {
-                storage[d] = src[s++];
-            }
-        }
+    protected void storeAtImpl(byte value, int byteIndex) {
+        // Implement directly and don't ask whether read-only
+        storage[byteIndex] = value;
     }
 
     /**
@@ -84,17 +72,17 @@
      * slice.
      */
     @Override
-    public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {
+    public PyBuffer getBufferSlice(int flags, int start, int count, int stride) {
 
-        if (length > 0) {
+        if (count > 0) {
             // Translate start relative to underlying buffer
             int compStride = this.stride * stride;
             int compIndex0 = index0 + start * this.stride;
             // Construct a view, taking a lock on the root object (this or this.root)
-            return new SlicedView(getRoot(), flags, storage, compIndex0, length, compStride);
+            return new SlicedView(getRoot(), flags, storage, compIndex0, count, compStride);
 
         } else {
-            // Special case for length==0 where above logic would fail. Efficient too.
+            // Special case for count==0 where above logic would fail. Efficient too.
             return new ZeroByteBuffer.View(getRoot(), flags);
         }
     }
@@ -115,14 +103,14 @@
          * @param flags consumer requirements
          * @param storage raw byte array containing exported data
          * @param index0 index into storage of item[0]
-         * @param len number of items in the slice
+         * @param count number of items in the sliced view
          * @param stride in between successive elements of the new PyBuffer
          * @throws PyException (BufferError) when expectations do not correspond with the type
          */
-        public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int len, int stride)
-                throws PyException {
+        public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int count,
+                int stride) throws PyException {
             // Create a new on the buffer passed in (part of the root)
-            super(flags, storage, index0, len, stride);
+            super(flags, root.getObj(), storage, index0, count, stride);
             // Get a lease on the root PyBuffer (writable)
             this.root = root.getBuffer(FULL);
         }
@@ -131,13 +119,5 @@
         protected PyBuffer getRoot() {
             return root;
         }
-
-        @Override
-        public void releaseAction() {
-            // We have to release the root too if ours was final.
-            root.release();
-        }
-
     }
-
 }
diff --git a/src/org/python/core/buffer/ZeroByteBuffer.java b/src/org/python/core/buffer/ZeroByteBuffer.java
--- a/src/org/python/core/buffer/ZeroByteBuffer.java
+++ b/src/org/python/core/buffer/ZeroByteBuffer.java
@@ -1,5 +1,6 @@
 package org.python.core.buffer;
 
+import org.python.core.BufferProtocol;
 import org.python.core.PyBuffer;
 import org.python.core.PyException;
 
@@ -12,29 +13,32 @@
  * 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 {
+public class ZeroByteBuffer extends BaseArrayBuffer {
 
     /** Shared instance of a zero-length storage. */
     private static final byte[] EMPTY = 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.
+     * read-only through {@link #isReadonly()} or as having a backing array through
+     * {@link #hasArray()}. These properties are moot, as any attempt to write to the pretended
+     * backing array produces an {@link IndexOutOfBoundsException}, but it is less surprising for
+     * client code that may ask, if the results are customary for the exporting object.
      *
      * @param flags consumer requirements
-     * @param readonly set true if readonly
-     * @throws PyException (BufferError) when expectations do not correspond with the type
+     * @param obj exporting object (or <code>null</code>)
+     * @param readonly set true if not to be considered writable
+     * @param hasArray set true if to be considered as backed by an array
+     * @throws PyException (BufferError) when client expectations do not correspond with the type
      */
-    public ZeroByteBuffer(int flags, boolean readonly) throws PyException {
-        super(CONTIGUITY | SIMPLE | (readonly ? 0 : WRITABLE));
-        this.storage = EMPTY;                       // Empty array
-        this.shape = SHAPE;                         // {0}
-        this.strides = SimpleBuffer.SIMPLE_STRIDES; // {1}
+    public ZeroByteBuffer(int flags, BufferProtocol obj, boolean readonly, boolean hasArray)
+            throws PyException {
+        super(EMPTY, CONTIGUITY | (readonly ? 0 : WRITABLE), 0, 0, 1);
+        this.obj = obj;
+        if (!hasArray) {
+            // super() knows we have an array, but this truth is inconvenient here.
+            removeFeatureFlags(AS_ARRAY);
+        }
         checkRequestFlags(flags);
     }
 
@@ -47,7 +51,7 @@
      * In a ZeroByteBuffer, the index is always out of bounds.
      */
     @Override
-    protected int calcIndex(int index) throws IndexOutOfBoundsException {
+    public int byteIndex(int index) throws IndexOutOfBoundsException {
         // This causes all access to the bytes in to throw (since BaseBuffer calls it).
         throw new IndexOutOfBoundsException();
     }
@@ -56,7 +60,7 @@
      * In a ZeroByteBuffer, if the dimensions are right, the index is out of bounds anyway.
      */
     @Override
-    protected int calcIndex(int... indices) throws IndexOutOfBoundsException {
+    public int byteIndex(int... indices) throws IndexOutOfBoundsException {
         // Bootless dimension check takes precedence (for consistency with other buffers)
         checkDimension(indices);
         // This causes all access to the bytes to throw (since BaseBuffer calls it).
@@ -79,30 +83,34 @@
      * In a ZeroByteBuffer, there is simply nothing to copy.
      */
     @Override
-    public void copyTo(int srcIndex, byte[] dest, int destPos, int length)
+    public void copyTo(int srcIndex, byte[] dest, int destPos, int count)
             throws IndexOutOfBoundsException, PyException {
         // Nothing to copy
     }
 
     /**
-     * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source length
-     * is zero.
+     * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source count is
+     * zero.
      */
     @Override
-    public void copyFrom(byte[] src, int srcPos, int destIndex, int length)
+    public void copyFrom(byte[] src, int srcPos, int destIndex, int count)
             throws IndexOutOfBoundsException, PyException {
-        if (length > 0) {
+        if (this.isReadonly()) {
+            throw notWritable();
+        } else if (count > 0) {
             throw new IndexOutOfBoundsException();
         }
     }
 
     /**
-     * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source length
-     * is zero.
+     * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source count is
+     * zero.
      */
     @Override
     public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException {
-        if (src.getLen() > 0) {
+        if (this.isReadonly()) {
+            throw notWritable();
+        } else if (src.getLen() > 0) {
             throw new IndexOutOfBoundsException();
         }
     }
@@ -112,8 +120,8 @@
      * as a result, with the export count incremented.
      */
     @Override
-    public PyBuffer getBufferSlice(int flags, int start, int length) {
-        if (start == 0 && length <= 0) {
+    public PyBuffer getBufferSlice(int flags, int start, int count) {
+        if (start == 0 && count <= 0) {
             return this.getBuffer(flags);
         } else {
             throw new IndexOutOfBoundsException();
@@ -125,9 +133,9 @@
      * 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);
+    public PyBuffer getBufferSlice(int flags, int start, int count, int stride) {
+        // It can't matter what the stride is since count is zero, or there's an error.
+        return getBufferSlice(flags, start, count);
     }
 
     /**
@@ -135,6 +143,7 @@
      * <p>
      * The implementation in <code>ZeroByteBuffer</code> efficiently returns an empty buffer.
      */
+    @SuppressWarnings("deprecation")
     @Override
     public Pointer getBuf() {
         // Has to be new because the client is allowed to manipulate the contents.
@@ -170,7 +179,7 @@
          */
         public View(PyBuffer root, int flags) {
             // Create a new ZeroByteBuffer on who-cares-what byte array
-            super(flags, root.isReadonly());
+            super(flags, root.getObj(), root.isReadonly(), root.hasArray());
             // But we still have to get a lease on the root PyBuffer
             this.root = root.getBuffer(FULL_RO);
         }
@@ -179,11 +188,5 @@
         protected PyBuffer getRoot() {
             return root;
         }
-
-        @Override
-        public void releaseAction() {
-            // We have to release the root too if ours was final.
-            root.release();
-        }
     }
 }
diff --git a/src/org/python/modules/_io/PyIOBase.java b/src/org/python/modules/_io/PyIOBase.java
--- a/src/org/python/modules/_io/PyIOBase.java
+++ b/src/org/python/modules/_io/PyIOBase.java
@@ -778,7 +778,7 @@
                 // None of the above: complain
                 throw tailoredTypeError("read-write buffer", obj);
             }
-            return new SimpleStringBuffer(PyBUF.SIMPLE, s);
+            return new SimpleStringBuffer(PyBUF.SIMPLE, null, s);
         }
     }
 
@@ -910,18 +910,17 @@
             + "fp is closed after the suite of the with statement is complete:\n" + "\n"
             + "with open('spam.txt', 'r') as fp:\n" + "    fp.write('Spam and eggs!')\n";
 
-
     /* Traverseproc implementation */
     @Override
     public int traverse(Visitproc visit, Object arg) {
-    	//closer cannot be null
+        // closer cannot be null
         if (closer.sys != null) {
-        	int retVal = visit.visit(closer.sys, arg);
+            int retVal = visit.visit(closer.sys, arg);
             if (retVal != 0) {
                 return retVal;
             }
         }
-        //__dict__ cannot be null
+        // __dict__ cannot be null
         return visit.visit(__dict__, arg);
     }
 
diff --git a/tests/java/org/python/core/BaseBytesTest.java b/tests/java/org/python/core/BaseBytesTest.java
--- a/tests/java/org/python/core/BaseBytesTest.java
+++ b/tests/java/org/python/core/BaseBytesTest.java
@@ -203,6 +203,7 @@
      *
      * @see junit.framework.TestCase#setUp()
      */
+    @Override
     protected void setUp() throws Exception {
         super.setUp();
         random = new Random(20120310L);
@@ -595,7 +596,7 @@
          */
         public MyBytes(BufferProtocol value) {
             super(TYPE);
-            init((BufferProtocol)value.getBuffer(PyBUF.SIMPLE));
+            init(value.getBuffer(PyBUF.SIMPLE));
         }
 
         /**
@@ -787,7 +788,7 @@
 
         @Override
         public PyBuffer getBuffer(int flags) {
-            return new SimpleBuffer(flags, store);
+            return new SimpleBuffer(flags, this, store);
         }
 
     }
@@ -824,7 +825,7 @@
                 return;
             }
             for (int i = 0; i < n; i++) {
-                int c = 0xff & ((int)s[pos + i]);
+                int c = 0xff & (s[pos + i]);
                 if (c == 0) {
                     c = '.';
                 } else if (Character.isISOControl(c)) {
diff --git a/tests/java/org/python/core/ByteBufferTestSupport.java b/tests/java/org/python/core/ByteBufferTestSupport.java
new file mode 100644
--- /dev/null
+++ b/tests/java/org/python/core/ByteBufferTestSupport.java
@@ -0,0 +1,585 @@
+package org.python.core;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Common apparatus for tests involving <code>byte[]</code> and <code>java.nio.ByteBuffer</code>
+ * material, in particular the tests of {@link PyBuffer} implementations. A test case that extends
+ * this class will be equipped with additional assertion methods and a class to represent
+ * <code>byte[]</code> test material in several forms.
+ */
+public class ByteBufferTestSupport {
+
+    /**
+     * Class to hold test material representing the same sequence of values 0..255 in several
+     * different ways.
+     */
+    protected static class ByteMaterial {
+
+        /** Length in bytes (length of every array in this material). */
+        final int length;
+        /** The byte values. */
+        byte[] bytes;
+        /** The byte values individually as ints. */
+        int[] ints;
+        /** The byte values treated as characters (unicode < 256). */
+        String string;
+
+        /** Construct from int array. */
+        public ByteMaterial(int[] a) {
+            ints = a.clone();
+            length = replicate();
+        }
+
+        /** Construct from String. */
+        public ByteMaterial(String s) {
+            ints = new int[s.length()];
+            for (int i = 0; i < ints.length; i++) {
+                ints[i] = 0xff & s.charAt(i);
+            }
+            length = replicate();
+        }
+
+        /** Construct from byte array. */
+        public ByteMaterial(byte[] b) {
+            ints = new int[b.length];
+            for (int i = 0; i < ints.length; i++) {
+                ints[i] = 0xff & b[i];
+            }
+            length = replicate();
+        }
+
+        /** Construct from pattern on values (used modulo 256). */
+        public ByteMaterial(int start, int count, int inc) {
+            ints = new int[count];
+            int x = start;
+            for (int i = 0; i < count; i++) {
+                ints[i] = x;
+                x = (x + inc) & 0xff;
+            }
+            length = replicate();
+        }
+
+        /**
+         * Once the integer value array {@link #ints} has been initialised, fill the others from it.
+         *
+         * @return length of (all) arrays in units
+         */
+        private int replicate() {
+            int n = ints.length;
+            bytes = new byte[n];
+            StringBuilder sb = new StringBuilder(n);
+
+            for (int i = 0; i < n; i++) {
+                int x = ints[i];
+                bytes[i] = (byte)x;
+                sb.appendCodePoint(x);
+            }
+            string = sb.toString();
+            return n;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(100);
+            sb.append("byte[").append(length).append("]={ ");
+            for (int i = 0; i < length; i++) {
+                if (i > 0) {
+                    sb.append(", ");
+                }
+                if (i >= 5) {
+                    sb.append(" ...");
+                    break;
+                } else {
+                    sb.append(ints[i]);
+                }
+            }
+            sb.append(" }");
+            return sb.toString();
+        }
+
+        /**
+         * @return a copy of the bytes array (that the client is allowed to modify)
+         */
+        byte[] getBytes() {
+            return bytes.clone();
+        }
+
+        /**
+         * @return a buffer on a copy of the bytes array (that the client is allowed to modify)
+         */
+        ByteBuffer getBuffer() {
+            return ByteBuffer.wrap(getBytes());
+        }
+
+        /**
+         * Create material equivalent to a slice of this material. This may act as a reference
+         * result for testing slice operations.
+         *
+         * @param start first byte-index to include
+         * @param length number of items
+         * @param stride between byte-indices
+         * @return ByteMaterial in which the arrays are a slice of this one
+         */
+        ByteMaterial slice(int start, int length, int stride) {
+            return new ByteMaterial(sliceBytes(bytes, 1, start, length, stride));
+        }
+
+        /**
+         * Create material equivalent to a slice of this material. This may act as a reference
+         * result for testing slice operations.
+         *
+         * @param start first byte-index to include
+         * @param itemsize number of consecutive bytes treated as one item
+         * @param length number of items
+         * @param stride between byte-indices
+         * @return ByteMaterial in which the arrays are a slice of this one
+         */
+        ByteMaterial slice(int itemsize, int start, int length, int stride) {
+            return new ByteMaterial(sliceBytes(bytes, itemsize, start, length, stride));
+        }
+    }
+
+    /**
+     * Create a byte array that is a strided copy of the one passed in. The specifications are
+     * assumed correct for the size of that array.
+     *
+     * @param b source array
+     * @param start first index to include
+     * @param length number of indices
+     * @param stride between indices
+     * @return slice of b
+     */
+    protected static byte[] sliceBytes(byte[] b, int start, int length, int stride) {
+        return sliceBytes(b, 1, start, length, stride);
+    }
+
+    /**
+     * Create a multi-byte item array that is a strided copy of the one passed in. The
+     * specifications are assumed correct for the size of that array.
+     *
+     * @param b source array
+     * @param itemsize number of consecutive bytes treated as one item
+     * @param start first byte-index to include
+     * @param length number of indices to visit (items to copy)
+     * @param stride between byte-indices
+     * @return slice of b
+     */
+    protected static byte[] sliceBytes(byte[] b, int itemsize, int start, int length, int stride) {
+        byte[] a = new byte[length];
+        for (int i = 0, j = start; i < length; i++, j += stride) {
+            for (int k = 0; k < itemsize; k++) {
+                a[i + k] = b[j + k];
+            }
+        }
+        return a;
+    }
+
+    /**
+     * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte
+     * array, when that <code>ByteBuffer</code> is obtained from a contiguous <code>PyBuffer</code>.
+     * Let <code>bb[i]</code> denote <code>bb.get(bb.position()+i)</code>, by analogy with a C
+     * pointer. It is required that <code>bb[k] == expected[k]</code>, for every index
+     * <code>k</code> in <code>expected</code>. If not, a <code>fail()</code> is declared.
+     *
+     * @param message to issue on failure
+     * @param expected expected byte array
+     * @param bb result to test
+     */
+    static void assertBytesEqual(String message, byte[] expected, ByteBuffer bb) {
+        // Use the position-advancing buffer get()
+        byte[] actual = new byte[expected.length];
+        bb.get(actual);
+        assertBytesEqual(message, expected, actual);
+    }
+
+    /**
+     * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte
+     * array, when that <code>ByteBuffer</code> is obtained from a striding <code>PyBuffer</code>.
+     * Let <code>bb[i]</code> denote <code>bb.get(bb.position()+i)</code>, by analogy with a C
+     * pointer. It is required that <code>bb[k*stride] == expected[k]</code>, for every index
+     * <code>k</code> in <code>expected</code>. If not, a <code>fail()</code> is declared.
+     *
+     * @param message to issue on failure
+     * @param expected expected byte array
+     * @param bb result to test
+     * @param stride in the buffer <code>bb</code>
+     */
+    static void assertBytesEqual(String message, byte[] expected, ByteBuffer bb, int stride) {
+        assertBytesEqual(message, expected, 0, expected.length, bb, stride);
+    }
+
+    /**
+     * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte
+     * array, when that <code>ByteBuffer</code> is obtained from a striding <code>PyBuffer</code>.
+     * Let <code>bb[i]</code> denote <code>bb.get(bb.position()+i)</code>, by analogy with a C
+     * pointer. It is required that <code>bb[k*stride] == expected[expectedStart+k]</code>, for
+     * <code>k=0</code> to <code>n-1</code>. If not, a <code>fail()</code> is declared.
+     *
+     * @param message to issue on failure
+     * @param expected expected byte array
+     * @param expectedStart where to start the comparison in <code>expected</code>
+     * @param n number of bytes to test
+     * @param bb result to test
+     * @param stride in the buffer <code>bb</code>
+     */
+    static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n,
+            ByteBuffer bb, int stride) {
+        // Note that this approach leaves the buffer position unmodified
+        int p = bb.position();
+        byte[] actual = new byte[n];
+        for (int k = 0; k < n; k++, p += stride) {
+            actual[k] = bb.get(p);
+        }
+        assertBytesEqual(message, expected, expectedStart, n, actual, 0);
+    }
+
+    /**
+     * Custom assert method comparing byte arrays: values in <code>actual[]</code> must match all
+     * those in <code>expected[]</code>, and they must be the same length.
+     *
+     * @param message to issue on failure
+     * @param expected expected byte array
+     * @param actual result to test
+     */
+    static void assertBytesEqual(String message, byte[] expected, byte[] actual) {
+        assertEquals(message + " (array size)", expected.length, actual.length);
+        assertBytesEqual(message, expected, 0, expected.length, actual, 0, 1);
+    }
+
+    /**
+     * Custom assert method comparing byte arrays. It is required that
+     * <code>actual[k] == expected[k]</code>, for <code>k=0</code> to <code>expected.length-1</code>
+     * . If not, a <code>fail()</code> is declared.
+     *
+     * @param message to issue on failure
+     * @param expected expected byte array
+     * @param actual result to test
+     * @param actualStart where to start the comparison in <code>actual</code>
+     */
+    static void assertBytesEqual(String message, byte[] expected, byte[] actual, int actualStart) {
+        assertBytesEqual(message, expected, 0, expected.length, actual, actualStart, 1);
+    }
+
+    /**
+     * Custom assert method comparing byte arrays. It is required that
+     * <code>actual[actualStart+k] == expected[expectedStart+k]</code>, for <code>k=0</code> to
+     * <code>n-1</code>. If not, a <code>fail()</code> is declared.
+     *
+     * @param message to issue on failure
+     * @param expected expected byte array
+     * @param expectedStart where to start the comparison in <code>expected</code>
+     * @param n number of bytes to test
+     * @param actual result to test
+     * @param actualStart where to start the comparison in <code>actual</code>
+     */
+    protected static void assertBytesEqual(String message, byte[] expected, int expectedStart,
+            int n, byte[] actual, int actualStart) {
+        assertBytesEqual(message, expected, expectedStart, n, actual, actualStart, 1);
+    }
+
+    /**
+     * Custom assert method comparing byte arrays. It is required that
+     * <code>actual[actualStart+k*stride] == expected[expectedStart+k]</code>, for <code>k=0</code>
+     * to <code>n-1</code>. If not, a <code>fail()</code> is declared.
+     *
+     * @param message to issue on failure
+     * @param expected expected byte array
+     * @param expectedStart where to start the comparison in <code>expected</code>
+     * @param n number of bytes to test
+     * @param actual result to test
+     * @param actualStart where to start the comparison in <code>actual</code>
+     * @param stride spacing of bytes in <code>actual</code> array
+     */
+    static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n,
+            byte[] actual, int actualStart, int stride) {
+
+        if (actualStart < 0) {
+            fail(message + " (start<0 in result)");
+
+        } else if (expectedStart < 0) {
+            fail(message + " (start<0 in expected result): bug in test?");
+
+        } else if (actualStart + (n - 1) * stride + 1 > actual.length) {
+            fail(message + " (result too short)");
+
+        } else if (expectedStart + n > expected.length) {
+            fail(message + " (expected result too short): bug in test?");
+
+        } else {
+            // Should be safe to compare the values
+            int i = actualStart, j, jLimit = expectedStart + n;
+            for (j = expectedStart; j < jLimit; j++) {
+                if (actual[i] != expected[j]) {
+                    break;
+                }
+                i += stride;
+            }
+
+            // If we stopped early, diagnose the problem
+            if (j < jLimit) {
+                byte[] a = Arrays.copyOfRange(actual, actualStart, actualStart + n);
+                byte[] e = Arrays.copyOfRange(expected, expectedStart, expectedStart + n);
+                System.out.println("  expected:" + Arrays.toString(e));
+                System.out.println("    actual:" + Arrays.toString(a));
+                System.out.println("  _actual_:" + Arrays.toString(actual));
+                fail(message + " (byte at " + j + ")");
+            }
+        }
+    }
+
+    /**
+     * Customised assert method comparing a int arrays: values in the actual value starting at
+     * actual[offset] must match all those in expected[], and there must be enough of them.
+     *
+     * @param message to issue on failure
+     * @param expected expected array
+     * @param actual result to test
+     * @param offset where to start the comparison in actual
+     */
+    static void assertIntsEqual(String message, int[] expected, int[] actual, int offset) {
+        int n = expected.length;
+        if (offset < 0) {
+            fail(message + " (offset<0)");
+        } else if (offset + n > actual.length) {
+            fail(message + " (too short)");
+        } else {
+            // Should be safe to compare the values
+            int i = offset, j;
+            for (j = 0; j < n; j++) {
+                if (actual[i++] != expected[j]) {
+                    break;
+                }
+            }
+            if (j < n) {
+                System.out.println("  expected:" + Arrays.toString(expected));
+                System.out.println("    actual:" + Arrays.toString(actual));
+                fail(message + " (int at " + j + ")");
+            }
+        }
+    }
+
+    /**
+     * Customised assert method comparing a int arrays: int in the actual value must match all those
+     * in expected[], and there must be the same number of them.
+     *
+     * @param message to issue on failure
+     * @param expected expected array
+     * @param actual result to test
+     */
+    protected static void assertIntsEqual(String message, int[] expected, int[] actual) {
+        int n = expected.length;
+        assertEquals(message, n, actual.length);
+        // Should be safe to compare the values
+        int j;
+        for (j = 0; j < n; j++) {
+            if (actual[j] != expected[j]) {
+                break;
+            }
+        }
+        if (j < n) {
+            System.out.println("  expected:" + Arrays.toString(expected));
+            System.out.println("    actual:" + Arrays.toString(actual));
+            fail(message + " (int at " + j + ")");
+        }
+    }
+
+    /**
+     * Method comparing byte arrays after a read (or view creation) operation involving a slice.
+     * <p>
+     * The invariant asserted must be explained carefully because of its generality. Suppose there
+     * to be three arrays of bytes <i>a</i>, <i>b</i> and <i>c</i>. Let <i>a</i> represent the state
+     * of some byte storage of length <i>L</i> before an operation. Let <i>b</i> represent the state
+     * of the same storage after an operation. Let <i>c</i> be related as follows.
+     * <p>
+     * <i>c</i> is the result, representing <i>n</i> blocks of <i>u</i> bytes copied from the
+     * storage, the <i>k</i>th block starting at position <i>s+kp</i> in the storage and at
+     * <i>t+ku</i> in <i>c</i>. <i>c</i> is of length <i>M≥nu</i>, and we assume
+     * <i>0≤s+kp<L</i>. After a read operation, it is required that:
+     * <ol>
+     * <li><i>c[t+iu+j] = b[s+ip+j]</i> for <i>0≤i<n</i> and <i>0≤j<u</i>, and</li>
+     * <li><i>a[k] = b[k]</i> for <i>0≤k<L</i>.
+     * </ol>
+     * <p>
+     *
+     * @param a storage state before the operation (typically reference data)
+     * @param b storage state after the operation (typically from object under test)
+     * @param c bytes read
+     * @param t index in <code>c</code> of the start byte of item 0
+     * @param n number of items
+     * @param u number of consecutive bytes per item
+     * @param s index in <code>b</code> of the start byte of item 0
+     * @param p the distance in <code>b</code> between the start bytes of successive items
+     */
+    static void checkReadCorrect(byte[] a, byte[] b, byte[] c, int t, int n, int u, int s, int p) {
+        // Check the b is the same as a
+        assertEquals("Storage size differs from reference", a.length, b.length);
+        for (int k = 0; k < b.length; k++) {
+            if (a[k] != b[k]) {
+                fail("Stored data changed during read");
+            }
+        }
+        // Check slice read out
+        checkEqualInSlice(b, c, t, n, u, s, p);
+    }
+
+    /**
+     * Method comparing byte arrays where a change operation has taken place involving a slice.
+     * <p>
+     * The invariant asserted must be explained carefully because of its generality. Suppose there
+     * to be three arrays of bytes <i>a</i>, <i>b</i> and <i>c</i>. Let <i>a</i> represent the state
+     * of some byte storage of length <i>L</i> before an operation. Let <i>b</i> represent the state
+     * of the same storage after an operation. Let <i>c</i> be related as follows.
+     * <p>
+     * <i>c</i> is the source, contaning at index <i>t</i>, <i>n</i> blocks of <i>u</i> bytes copied
+     * to the storage. As before, the <i>k</i>th block starts at position <i>s+kp</i> in the storage
+     * and at <i>t+ku</i> in <i>c</i>. <i>c</i> is of length <i>M≥t+nu</i>, and we assume
+     * <i>0≤s+kp<L</i>. After a write operation, it is required that:
+     * <ol>
+     * <li><i>c[t+iu+j] = b[s+ip+j]</i> for <i>0≤i<n</i> and <i>0≤j<u</i>, and</li>
+     * <li><i>a[k] = b[k]</i> for <i>0≤k<L</i> and <i>k≠s+ip+j</i> for any choice of <i</i>
+     * and <i>j</i> where <i>0≤i<n</i> and <i>0≤j<u</i>.
+     * </ol>
+     * Note that the first of these is the same as for a read and the second requires equality
+     * "everywhere else".
+     *
+     * @param a storage state before the operation (typically reference data)
+     * @param b storage state after the operation (typically from object under test)
+     * @param c bytes written
+     * @param t index in <code>c</code> of the start byte of item 0
+     * @param n number of items
+     * @param u number of consecutive bytes per item
+     * @param s index in <code>b</code> of the start byte of item 0
+     * @param p the distance in <code>b</code> between the start bytes of successive items
+     */
+    static void checkWriteCorrect(byte[] a, byte[] b, byte[] c, int t, int n, int u, int s, int p) {
+        assertEquals("Stored size has changed", a.length, b.length);
+        checkEqualInSlice(b, c, t, n, u, s, p);
+        checkUnchangedElsewhere(a, b, n, u, s, p);
+    }
+
+    /**
+     * Method comparing bytes in a slice pattern of one byte array to bytes taken consecutively in
+     * another array. This is needed in testing when bytes have been copied into or out of an array
+     * slice.
+     * <p>
+     * Let <i>b</i> represent the state of the byte storage of length <i>L</i> after the copy
+     * operation (the sliced array). Let <i>c</i> be a source or destination array, a section of
+     * which at index <i>t</i> represents <i>n</i> blocks of <i>u</i> bytes copied to or from the
+     * storage. <i>c</i> is of length at least <i>t+nu</i>. The <i>k</i>th block starts at position
+     * <i>s+kp</i> in the storage <i>b</i> and at <i>t+ku</i> in <i>c</i>, and we assume
+     * <i>0≤s+kp<L</i>. After a write operation, it is required that: <i>c[t+iu+j] =
+     * b[s+ip+j]</i> for <i>0≤i<n</i> and <i>0≤j<u</i>.
+     *
+     *
+     * @param b storage state after the operation (typically from object under test)
+     * @param c bytes written
+     * @param t index in <code>c</code> of the start byte of item 0
+     * @param n number of items
+     * @param u number of consecutive bytes per item
+     * @param s index in <code>b</code> of the start byte of item 0
+     * @param p the distance in <code>b</code> between the start bytes of successive items
+     */
+    static void checkEqualInSlice(byte[] b, byte[] c, int t, int n, int u, int s, int p) {
+        // Check correct use of the test
+        checkSliceArgs(b, c, t, n, u, s, p);
+
+        // Check the data in copied units (and p-u bytes following)
+        for (int i = 0; i < n; i++) {
+            int ci = t + i * u, bi = s + i * p;
+            for (int j = 0; j < u; j++, bi++, ci++) {
+                // Compare corresponding bytes of this unit in c and b
+                if (c[ci] != b[bi]) {
+                    fail(String.format("contiguous data at %d not equal to buffer at %d", ci, bi));
+                }
+            }
+        }
+    }
+
+    /**
+     * Method comparing the before and after state of the parts of a byte array that should be
+     * untouched where a change operation has taken place involving a slice.
+     * <p>
+     * Let <i>a</i> represent the state of some byte storage of length <i>L</i> before an operation.
+     * Let <i>b</i> represent the state of the same storage after an operation. After a write
+     * operation, it is required that: <i>a[k] = b[k]</i> for <i>0≤k<L</i> and
+     * <i>k≠s+ip+j</i> for any choice of <i</i> and <i>j</i> where <i>0≤i<n</i> and
+     * <i>0≤j<u</i>.
+     * <p>
+     * Note that requires equality "everywhere else" than in the slice defined by <i>n</i> units of
+     * size <i>u</i> starting at <i>s</i>.
+     *
+     * @param a storage state before the operation (typically reference data)
+     * @param b storage state after the operation (typically from object under test)
+     * @param n number of items
+     * @param u number of consecutive bytes per item
+     * @param s index in <code>b</code> of the start byte of item 0
+     * @param p the distance in <code>b</code> between the start bytes of successive items
+     */
+    static void checkUnchangedElsewhere(byte[] a, byte[] b, int n, int u, int s, int p) {
+        // Check correct use of the test
+        assertEquals("Stored size has changed", a.length, b.length);
+        assertFalse("Unit size exceeds spacing", u > p && u + p > 0);
+        String bufferChangedAt = "buffer changed at %d (outside slice)";
+
+        int absp, low, high;
+
+        if (n < 1) {
+            // No elements: check whole array.
+            absp = low = high = 0;
+        } else if (p >= 0) {
+            // Stride is forwards in the range (easy case)
+            absp = p;
+            // Lowest byte index in the data is byte 0 of first unit in slice
+            low = s;
+            // One after highest byte index is just beyond last byte of last unit in slice
+            high = s + (n - 1) * p + u;
+        } else {
+            // p<0: stride is backwards in the range (delicate case)
+            absp = -p;
+            // Lowest byte index in the data is byte 0 of last unit in slice
+            low = s + (n - 1) * p;
+            // One after highest byte index is just beyond last byte of first unit in slice
+            high = s + u;
+        }
+
+        // Visit each copied unit (from low to high byte index) except the highest.
+        for (int i = 0; i < n - 1; i++) {
+            int bi = low + i * absp + u;
+            // Check there was no change to the absp-u bytes following unit in b
+            for (int j = u; j < absp; j++, bi++) {
+                if (b[bi] != a[bi]) {
+                    fail(String.format(bufferChangedAt, bi));
+                }
+            }
+        }
+
+        // Check that b[0:low] is unchanged
+        for (int k = 0; k < low; k++) {
+            if (b[k] != a[k]) {
+                fail(String.format(bufferChangedAt, k));
+            }
+        }
+
+        // Check that [high:] is unchanged
+        for (int k = Math.max(high, 0); k < b.length; k++) {
+            if (b[k] != a[k]) {
+                fail(String.format(bufferChangedAt, k));
+            }
+        }
+    }
+
+    /** Common code for <code>checkReadCorrect</code> and <code>checkWriteCorrect</code>. */
+    private static void checkSliceArgs(byte[] b, byte[] c, int t, int n, int u, int s, int p) {
+        // Check correct use of the test
+        assertFalse("Slice data less than n units", c.length < t + n * u);
+        assertFalse("Slice data exceeds destination", n * u > b.length);
+        assertFalse("Unit size exceeds spacing", u > p && u + p > 0);
+    }
+
+}
\ No newline at end of file
diff --git a/tests/java/org/python/core/PyBufferNIOTest.java b/tests/java/org/python/core/PyBufferNIOTest.java
new file mode 100644
--- /dev/null
+++ b/tests/java/org/python/core/PyBufferNIOTest.java
@@ -0,0 +1,305 @@
+package org.python.core;
+
+import java.lang.ref.WeakReference;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.runners.Parameterized.Parameters;
+import org.python.core.ByteBufferTestSupport.ByteMaterial;
+import org.python.core.PyBufferTestSupport.ExporterFactory;
+import org.python.core.PyBufferTestSupport.TestSpec;
+import org.python.core.PyBufferTestSupport.WritableExporterFactory;
+import org.python.core.buffer.BaseBuffer;
+import org.python.core.buffer.SimpleNIOBuffer;
+
+public class PyBufferNIOTest extends PyBufferTest {
+
+    public PyBufferNIOTest(TestSpec spec) {
+        super(spec);
+    }
+
+    /**
+     * Generate test data to be held in the testing framework and used to construct tests. This
+     * method is called once by the test framework. Each element of the returned collection is a
+     * specification that becomes the arguments to the constructor when JUnit prepares to invoke a
+     * test.
+     * <p>
+     * Internally, this method creates a small number of instances of the object types whose
+     * <code>PyBuffer</code> export mechanism is to be tested. Each is paired with a reference value
+     * represented in several forms. The <code>PyBufferTestSupport</code> class then multiplies
+     * these by creating a selection of feasible sliced views, the whole collection of root and
+     * slice objects being returned.
+     *
+     * @return generated list of test data
+     */
+    @Parameters
+    public static Collection<TestSpec[]> genTestSpecs() {
+
+        PyBufferTestSupport s = new PyBufferTestSupport(sliceLengths, sliceSteps);
+
+        // Tests using local types of exporter
+
+        ExporterFactory rollYourOwnExporter = new WritableExporterFactory() {
+
+            @Override
+            public BufferProtocol make(ByteMaterial m) {
+                return new RollYourOwnExporter(m.getBuffer());
+            }
+
+        };
+        s.add(rollYourOwnExporter, byteMaterial);
+        s.add(rollYourOwnExporter, emptyMaterial);
+
+        // All combinations of heap/direct, writable and empty/small/large (I'm so thorough!)
+
+        ExporterFactory readonlyHeapNIOExporter = new TestNIOExporterFactory(false, false);
+        s.add(readonlyHeapNIOExporter, emptyMaterial);
+        s.add(readonlyHeapNIOExporter, byteMaterial);
+        s.add(readonlyHeapNIOExporter, longMaterial);
+
+        ExporterFactory writableHeapNIOExporter = new TestNIOExporterFactory(true, false);
+        s.add(writableHeapNIOExporter, emptyMaterial);
+        s.add(writableHeapNIOExporter, byteMaterial);
+        s.add(writableHeapNIOExporter, longMaterial);
+
+        ExporterFactory readonlyDirectNIOExporter = new TestNIOExporterFactory(false, true);
+        s.add(readonlyDirectNIOExporter, emptyMaterial);
+        s.add(readonlyDirectNIOExporter, byteMaterial);
+        s.add(readonlyDirectNIOExporter, longMaterial);
+
+        ExporterFactory writableDirectNIOExporter = new TestNIOExporterFactory(true, true);
+        s.add(writableDirectNIOExporter, emptyMaterial);
+        s.add(writableDirectNIOExporter, byteMaterial);
+        s.add(writableDirectNIOExporter, longMaterial);
+
+        // Return the generated test data
+
+        List<TestSpec[]> ret = s.getTestData();
+        if (PRINT_KEY) {
+            int key = 0;
+            for (TestSpec[] r : ret) {
+                TestSpec spec = r[0];
+                System.out.printf("%6d : %s\n", key++, spec.toString());
+            }
+        }
+        return ret;
+    }
+
+    /*
+     * --------------------------------------------------------------------------------------------
+     * A series of custom exporters that use a java.nio.ByteBuffer to store and export their
+     * implementation data.
+     * --------------------------------------------------------------------------------------------
+     */
+    /**
+     * A class to act as an exporter that uses the SimpleBuffer. The exporter shares a single
+     * exported buffer between all consumers and needs to take any action immediately when that
+     * buffer is finally released. You are most likely to use this approach with an exporting object
+     * type that modifies its behaviour while there are active exports, but where it is worth
+     * avoiding the cost of duplicate buffers. This is the case with PyByteArray, which prohibits
+     * operations that would resize it, while there are outstanding exports.
+     */
+    private static class TestNIOExporter extends TestableExporter {
+
+        protected ByteBuffer storage;
+
+        /**
+         * Construct a simple exporter from the bytes supplied.
+         *
+         * @param storage
+         */
+        public TestNIOExporter(ByteBuffer storage) {
+            this.storage = storage;
+        }
+
+        @Override
+        public PyBuffer getBuffer(int flags) {
+            // If we have already exported a buffer it may still be available for re-use
+            BaseBuffer pybuf = getExistingBuffer(flags);
+            if (pybuf == null) {
+                // No existing export we can re-use
+                pybuf = new SimpleNIOBuffer(flags, this, storage) {
+
+                    @Override
+                    protected void releaseAction() {
+                        export = null; // Final release really is final (not reusable)
+                    }
+                };
+
+                // Hold a reference for possible re-use
+                export = new WeakReference<BaseBuffer>(pybuf);
+            }
+            return pybuf;
+        }
+
+    }
+
+    /**
+     * A factory for exporting objects to be used in the tests. These objects use a
+     * <code>ByteBuffer</code> for their exported representation, and the factory is programmed on
+     * creation to whether these buffers should be writable or direct.
+     */
+    static class TestNIOExporterFactory implements ExporterFactory {
+
+        final boolean writable;
+        final boolean isDirect;
+
+        TestNIOExporterFactory(boolean writable, boolean isDirect) {
+            this.writable = writable;
+            this.isDirect = isDirect;
+        }
+
+        public boolean isWritable() {
+            return writable;
+        }
+
+        public boolean isDirect() {
+            return isDirect;
+        }
+
+        @Override
+        public BufferProtocol make(ByteMaterial m) {
+            ByteBuffer bb = m.getBuffer();
+            if (isDirect) {
+                // Replace bb with a direct buffer containing the same bytes
+                ByteBuffer direct = ByteBuffer.allocateDirect(bb.capacity());
+                direct.put(bb).flip();
+                bb = direct;
+            }
+            if (!writable) {
+                bb = bb.asReadOnlyBuffer();
+            }
+            return new TestNIOExporter(bb);
+        }
+
+        @Override
+        public boolean isReadonly() {
+            return !writable;
+        }
+
+        @Override
+        public boolean hasArray() {
+            return !isDirect && writable;
+        }
+
+    }
+
+    /** A class to act as an exporter that uses the RollYourOwnNIOBuffer class. */
+    private static class RollYourOwnExporter extends TestableExporter {
+
+        protected ByteBuffer storage;
+
+        public RollYourOwnExporter(ByteBuffer storage) {
+            this.storage = storage;
+        }
+
+        @Override
+        public PyBuffer getBuffer(int flags) {
+            // If we have already exported a buffer it may still be available for re-use
+            BaseBuffer pybuf = getExistingBuffer(flags);
+            if (pybuf == null) {
+                // No existing export we can re-use
+                pybuf = new RollYourOwnNIOBuffer(flags, this, storage);
+                // Hold a reference for possible re-use
+                export = new WeakReference<BaseBuffer>(pybuf);
+            }
+            return pybuf;
+        }
+
+    }
+
+    /**
+     * Minimal extension of BaseBuffer in order to test the default implementations there. They're
+     * slow, so mostly we override them in the implementations BaseArrayBuffer and BaseNIOBuffer,
+     * but they still have to be correct. The class represents a one-dimensional, strided array of
+     * bytes, so it can represent a slice of itself.
+     */
+    private static class RollYourOwnNIOBuffer extends BaseBuffer {
+
+        final static int FEATURES = PyBUF.WRITABLE | PyBUF.AS_ARRAY;
+
+        final ByteBuffer storage;
+        final PyBuffer root;
+
+        /**
+         * Create a buffer view of a given <code>ByteBuffer</code> in which the data is the
+         * contiguous sequence of bytes from the position to the limit.
+         *
+         * @param flags consumer requirements
+         * @param obj exporting object (or <code>null</code>)
+         * @param storage buffer exported (from the position to the limit)
+         */
+        public RollYourOwnNIOBuffer(int flags, BufferProtocol obj, ByteBuffer storage) {
+            this(flags, null /* =this */, obj, storage, storage.position(), storage.remaining(), 1);
+        }
+
+        /**
+         * Construct a slice of a one-dimensional byte buffer.
+         *
+         * @param flags consumer requirements
+         * @param obj exporting object (or <code>null</code>)
+         * @param root on which release must be called when this is released
+         * @param storage buffer containing exported data
+         * @param index0 index into storage of item[0]
+         * @param count number of items in the slice
+         * @param stride in between successive elements of the new PyBuffer
+         * @throws PyException (BufferError) when expectations do not correspond with the type
+         */
+        public RollYourOwnNIOBuffer(int flags, PyBuffer root, BufferProtocol obj,
+                ByteBuffer storage, int index0, int count, int stride)
+                throws IndexOutOfBoundsException, NullPointerException, PyException {
+            // Client will need to navigate using shape and strides if this is a slice
+            super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : STRIDES), //
+                    index0, new int[] {count}, new int[] {stride});
+            this.storage = storage.duplicate();
+
+            // Check the potential index range
+            if (count > 0) {
+                int end = index0 + (count - 1) * stride;
+                final int END = storage.capacity() - 1;
+                if (index0 < 0 || index0 > END || end < 0 || end > END) {
+                    throw new IndexOutOfBoundsException();
+                }
+            }
+            // Check client is compatible
+            checkRequestFlags(flags);
+            // Get a lease on the root PyBuffer (read-only). Last in case a check above fails.
+            if (root == null) {
+                this.root = this;
+                this.obj = obj;
+            } else {
+                this.root = root.getBuffer(FULL_RO);
+                this.obj = root.getObj();
+            }
+        }
+
+        @Override
+        protected PyBuffer getRoot() {
+            return root;
+        }
+
+        @Override
+        public PyBuffer getBufferSlice(int flags, int start, int count, int stride) {
+            int newStart = index0 + start * strides[0];
+            int newStride = strides[0] * stride;
+            return new RollYourOwnNIOBuffer(flags, root, null, storage, newStart, count, newStride);
+        }
+
+        @Override
+        public ByteBuffer getNIOByteBufferImpl() {
+            return storage.duplicate();
+        }
+
+        @Override
+        protected byte byteAtImpl(int byteIndex) {
+            return storage.get(byteIndex);
+        }
+
+        @Override
+        protected void storeAtImpl(byte value, int byteIndex) throws IndexOutOfBoundsException,
+                PyException {
+            storage.put(byteIndex, value);
+        }
+    }
+}
diff --git a/tests/java/org/python/core/PyBufferTest.java b/tests/java/org/python/core/PyBufferTest.java
--- a/tests/java/org/python/core/PyBufferTest.java
+++ b/tests/java/org/python/core/PyBufferTest.java
@@ -1,16 +1,28 @@
 package org.python.core;
 
+import static org.junit.Assert.*;
+import static org.python.core.ByteBufferTestSupport.assertIntsEqual;
+import static org.python.core.PyBufferTestSupport.bytesFromByteAt;
+
 import java.lang.ref.Reference;
 import java.lang.ref.SoftReference;
 import java.lang.ref.WeakReference;
+import java.nio.ByteBuffer;
 import java.util.Arrays;
-import java.util.HashSet;
-import java.util.LinkedList;
+import java.util.Collection;
 import java.util.List;
-import java.util.Set;
 
-import junit.framework.TestCase;
-
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.python.core.ByteBufferTestSupport.ByteMaterial;
+import org.python.core.PyBufferTestSupport.ExporterFactory;
+import org.python.core.PyBufferTestSupport.ReadonlyExporterFactory;
+import org.python.core.PyBufferTestSupport.SlicedTestSpec;
+import org.python.core.PyBufferTestSupport.TestSpec;
+import org.python.core.PyBufferTestSupport.TestSpec.ObjectAndView;
+import org.python.core.PyBufferTestSupport.WritableExporterFactory;
 import org.python.core.buffer.BaseBuffer;
 import org.python.core.buffer.SimpleBuffer;
 import org.python.core.buffer.SimpleStringBuffer;
@@ -21,16 +33,18 @@
  * Test the several implementations (and exporters) of the PyBuffer interface provided in the Jython
  * core.
  * <p>
- * The approach is to create test material once that has the necessary variety in byte array values,
- * then for each test, when the JUnit framework creates an instance of the function-specific test,
- * to use this material to create instances of each read-only type and each writable type. Writable
- * instance types go onto the lists buffersToRead and buffersToWrite, while read-only instances go
- * onto the lists buffersToRead and buffersToFailToWrite.
+ * The approach is to create test material once (static initialisation) that has the necessary
+ * variety in byte array values. From these raw values, during a phase of (static) initialisation
+ * invoked by the JUnit framework, we create a rich set of root objects, and slices made from them,
+ * paired with the value those buffer views should have, represented as byte[] (and a few other
+ * types). These are <code>BufferTestPair</code> objects. The collection is the <b>test data</b>.
  * <p>
- * In general, tests of methods that read data apply themselves to all the elements of the
- * buffersToRead list, while tests of methods that write data apply themselves to all the elements
- * of the buffersToWrite list and check that members of the buffersToFailToWrite list raise an
- * exception.
+ * The JUnit framework will then construct an instance of this test using one
+ * <code>BufferTestPair</code> object from the test data, and call one test method. The
+ * initialisation of the test fixture with a <code>BufferTestPair</code> object provides the test
+ * method with a <code>PyBuffer</code> object on which to operate, and enough ancilliary information
+ * to deduce the expected outcome. In particular, it will be apparent whether write actions should
+ * succeed or raise an 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 and indirect array structures as buffers.
@@ -39,598 +53,754 @@
  * 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 {
+ at RunWith(Parameterized.class)
+public class PyBufferTest {
 
     /** Control amount of output. Instance variable so can be adjusted temporarily in test. */
     protected int verbosity = 0;
 
+    /** Print a list of the test material. (From JUnit 4.12 use Parameters(name)). */
+    protected static final boolean PRINT_KEY = true;
+
+    /** Size of some large arrays. */
+    static final int LONG = 1000;
+
+    /** Lengths we will use if we can when slicing view */
+    protected static final int[] sliceLengths = {1, 2, 5, 0, LONG / 4};
+
+    /** Step sizes we will use if we can when slicing view */
+    protected static final int[] sliceSteps = {1, 2, 3, 7};
+
+    /** Exception raising requires the Jython interpreter to be initialised **/
+    protected static PythonInterpreter interp = new PythonInterpreter();
+
+    /** The test material and a buffer created by the test-runner. */
+    protected TestSpec spec;
+    protected ByteMaterial ref;
+    protected BufferProtocol obj;
+    protected PyBuffer view;
+
     /**
-     * Generated constructor
+     * Construct an instance to run one test, using one set of test data.
      *
-     * @param name
+     * @param pair The test material and a buffer created by the test-runner.
      */
-    public PyBufferTest(String name) {
-        super(name);
+    public PyBufferTest(TestSpec spec) {
+        this.spec = spec;
+        ref = spec.ref;
+        createObjAndView();
     }
 
-    /** Sometimes we need the interpreter to be initialised **/
-    PythonInterpreter interp;
+    /**
+     * Create (or re-create) the test object and view from the specification. Test cases that call a
+     * mutator repeatedly must call this each time in order to work with clean objects.
+     */
+    protected void createObjAndView() {
+        TestSpec.ObjectAndView pair = spec.makePair();
+        obj = pair.obj;
+        view = pair.view;
+    }
 
     /*
      * Values for initialising the exporters.
      */
-    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]);
-    public static final int LONG = 1000;
-    private static final ByteMaterial longMaterial = new ByteMaterial(0, LONG, 5);
+    protected static final ByteMaterial byteMaterial = new ByteMaterial(10, 16, 3);
+    protected static final ByteMaterial abcMaterial = new ByteMaterial("abcdefgh");
+    protected static final ByteMaterial stringMaterial = new ByteMaterial("Mon côté fâcheux");
+    protected static final ByteMaterial emptyMaterial = new ByteMaterial(new byte[0]);
+    protected static final ByteMaterial longMaterial = new ByteMaterial(0, LONG, 5);
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    /**
+     * Generate test data to be held in the testing framework and used to construct tests. This
+     * method is called once by the test framework. Each element of the returned collection is a
+     * specification that becomes the arguments to the constructor when JUnit prepares to invoke a
+     * test.
+     * <p>
+     * Internally, this method creates a small number of instances of the object types whose
+     * <code>PyBuffer</code> export mechanism is to be tested. Each is paired with a reference value
+     * represented in several forms. The <code>PyBufferTestSupport</code> class then multiplies
+     * these by creating a selection of feasible sliced views, the whole collection of root and
+     * slice objects being returned.
+     *
+     * @return generated list of test data
+     */
+    @Parameters
+    public static Collection<TestSpec[]> genTestSpecs() {
 
-        // Exception raising requires the Jython interpreter
-        interp = new PythonInterpreter();
+        PyBufferTestSupport s = new PyBufferTestSupport(sliceLengths, sliceSteps);
 
         // 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);
+
+        ExporterFactory simpleExporter = new SimpleExporterFactory();
+        s.add(simpleExporter, byteMaterial);
+
+        ExporterFactory simpleWritableExporter = new WritableExporterFactory() {
+
+            @Override
+            public BufferProtocol make(ByteMaterial m) {
+                return new SimpleWritableExporter(m.getBytes());
+            }
+
+        };
+        s.add(simpleWritableExporter, abcMaterial);
+        s.add(simpleWritableExporter, emptyMaterial);
+
+        ExporterFactory stringExporter = new ReadonlyExporterFactory() {
+
+            @Override
+            public BufferProtocol make(ByteMaterial m) {
+                return new StringExporter(m.string);
+            }
+
+        };
+        s.add(stringExporter, stringMaterial);
+
+        // Tests with an buffer implementation directly extending BaseBuffer
+
+        ExporterFactory rollYourOwnExporter = new WritableExporterFactory() {
+
+            @Override
+            public BufferProtocol make(ByteMaterial m) {
+                return new RollYourOwnExporter(m.getBytes());
+            }
+
+        };
+        s.add(rollYourOwnExporter, byteMaterial);
+        s.add(rollYourOwnExporter, emptyMaterial);
 
         // Tests with PyByteArray
-        genWritable(new PyByteArray(abcMaterial.getBytes()), abcMaterial);
-        genWritable(new PyByteArray(longMaterial.getBytes()), longMaterial);
-        genWritable(new PyByteArray(), emptyMaterial);
+
+        ExporterFactory pyByteArrayExporter = new WritableExporterFactory() {
+
+            @Override
+            public BufferProtocol make(ByteMaterial m) {
+                return new PyByteArray(m.getBytes());
+            }
+
+        };
+        s.add(pyByteArrayExporter, byteMaterial);
+        s.add(pyByteArrayExporter, longMaterial);
+        s.add(pyByteArrayExporter, emptyMaterial);
 
         // Tests with PyString
-        genReadonly(new PyString(abcMaterial.string), abcMaterial);
-        genReadonly(new PyString(), emptyMaterial);
+
+        ExporterFactory pyStringExporter = new ReadonlyExporterFactory() {
+
+            @Override
+            public BufferProtocol make(ByteMaterial m) {
+                return new PyString(m.string);
+            }
+
+        };
+        s.add(pyStringExporter, abcMaterial);
+        s.add(pyStringExporter, 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;
-        genWritable(truncated, truncatedMaterial);
+
+        ExporterFactory offsetPyByteArrayExporter = new WritableExporterFactory() {
+
+            @Override
+            public BufferProtocol make(ByteMaterial m) {
+                // In this PyByteArray the data will not start at storage[0]
+                final int OFFSET = 4;
+                byte[] b = m.getBytes();
+                // Make a copy with padding at the start and wrap it in a bytearray
+                byte[] data = new byte[OFFSET + b.length];
+                System.arraycopy(b, 0, data, OFFSET, b.length);
+                PyByteArray a = new PyByteArray(data);
+                // This operation may (will) be implemented without data movement
+                a.delRange(0, OFFSET);
+                assert a.__alloc__() > b.length;
+                return a;
+            }
+
+        };
+        s.add(offsetPyByteArrayExporter, byteMaterial);
+        s.add(offsetPyByteArrayExporter, longMaterial);
+
+        // Return the generated test data
+
+        List<TestSpec[]> ret = s.getTestData();
+        if (PRINT_KEY) {
+            int key = 0;
+            for (TestSpec[] r : ret) {
+                TestSpec spec = r[0];
+                System.out.printf("%6d : %s\n", key++, spec.toString());
+            }
+        }
+        return ret;
     }
 
-    /** Generate a series of test material for a writable object. */
-    private void genWritable(BufferProtocol exporter, ByteMaterial material) {
-        generate(exporter, material, false);
+    /**
+     * Brevity allowing each test to announce itself by naming the part of the api tested.
+     *
+     * @param api naming the part of the api tested
+     */
+    protected void announce(String api) {
+        if (verbosity > 0) {
+            System.out.printf("%-30s %s\n", api + ":", spec.toString());
+        }
     }
 
-    /** Generate a series of test material for a read-only object. */
-    private void genReadonly(BufferProtocol exporter, ByteMaterial material) {
-        generate(exporter, material, true);
+    /** Test method for {@link org.python.core.PyBUF#isReadonly()}. */
+    @Test
+    public void testIsReadonly() {
+        announce("isReadonly");
+        assertTrue(view.isReadonly() == spec.readonly);
     }
 
-    /** Lengths we will use if we can when slicing view */
-    private static final int[] sliceLengths = {1, 2, 5, 0, LONG / 4};
+    /** Test method for {@link org.python.core.PyBUF#getNdim()}. */
+    @Test
+    public void testGetNdim() {
+        announce("getNdim");
+        // Only know how to do 1 dimension at the moment
+        assertEquals("unexpected ndim", spec.shape.length, view.getNdim());
+    }
 
-    /** Step sizes we will use if we can when slicing view */
-    private static final int[] sliceSteps = {1, 2, 3, 7};
+    /** Test method for {@link org.python.core.PyBUF#getShape()}. */
+    @Test
+    public void testGetShape() {
+        announce("getShape");
+        int[] shape = view.getShape();
+        assertNotNull("shape[] should always be provided", shape);
+        assertIntsEqual("unexpected shape", spec.shape, shape);
+    }
 
-    /**
-     * 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) {
+    /** Test method for {@link org.python.core.PyBUF#getLen()}. */
+    @Test
+    public void testGetLen() {
+        announce("getLen");
+        assertEquals("unexpected length", ref.length, view.getLen());
+    }
 
-        // Generate a test using the buffer directly exported by the exporter
-        PyBuffer direct = queue(exporter, material, readonly);
+    /** Test method for {@link org.python.core.PyBUF#getObj()}. */
+    @Test
+    public void testGetObj() {
+        announce("getObj");
+        assertEquals("unexpected exporting object", obj, view.getObj());
+    }
 
-        // 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
+    /** Test method for {@link org.python.core.PyBuffer#byteAt(int)}. */
+    @Test
+    public void testByteAt() {
+        announce("byteAt");
+        for (int i = 0; i < ref.length; i++) {
+            assertEquals(ref.bytes[i], view.byteAt(i));
+        }
+    }
 
-        // 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) {
+    /** Test method for {@link org.python.core.PyBuffer#byteAt(int[])}. */
+    @Test
+    public void testByteAtNdim() {
+        announce("byteAt (n-dim)");
+        int[] index = new int[1];
 
-                if (length == 0) {
-                    queue(direct, material, start, 0, 1, readonly);
-                    queue(direct, material, start, 0, 2, readonly);
+        if (view.getShape().length != 1) {
+            fail("Test not implemented if dimensions != 1");
+        }
+        // Run through 1D index for view
+        for (int i = 0; i < ref.length; i++) {
+            index[0] = i;
+            assertEquals(ref.bytes[i], view.byteAt(index));
+        }
 
-                } else if (length == 1 && start < N) {
-                    queue(direct, material, start, 1, 1, readonly);
-                    queue(direct, material, start, 1, 2, readonly);
+        // Check 2D index throws
+        try {
+            view.byteAt(0, 0);
+            fail("Use of 2D index did not raise exception");
+        } catch (PyException pye) {
+            // Expect BufferError
+            assertEquals(Py.BufferError, pye.type);
+        }
+    }
 
-                } else if (start < N) {
+    /** Test method for {@link org.python.core.PyBuffer#intAt(int)}. */
+    @Test
+    public void testIntAt() {
+        announce("intAt");
+        for (int i = 0; i < ref.length; i++) {
+            assertEquals(ref.ints[i], view.intAt(i));
+        }
+    }
 
-                    // 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);
-                        }
+    /** Test method for {@link org.python.core.PyBuffer#intAt(int[])}. */
+    @Test
+    public void testIntAtNdim() {
+        announce("intAt (n-dim)");
+        int[] index = new int[1];
+
+        if (view.getShape().length != 1) {
+            fail("Test not implemented for dimensions != 1");
+        }
+        // Run through 1D index for view
+        for (int i = 0; i < ref.length; i++) {
+            index[0] = i;
+            assertEquals(ref.ints[i], view.intAt(index));
+        }
+        // Check 2D index throws
+        try {
+            view.intAt(0, 0);
+            fail("Use of 2D index did not raise exception");
+        } catch (PyException pye) {
+            // Expect BufferError
+            assertEquals(Py.BufferError, pye.type);
+        }
+    }
+
+    /** Test method for {@link org.python.core.PyBuffer#storeAt(byte, int)}. */
+    @Test
+    public void testStoreAt() {
+        announce("storeAt");
+        int n = ref.length;
+        int[] exp = ref.ints.clone();
+        if (!spec.readonly) {
+            // 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
+                view.storeAt(v, i);
+            }
+            // Compare each location with modified test data using intAt()
+            for (int i = 0; i < n; i++) {
+                assertEquals(exp[i] ^ 3, view.intAt(i));
+            }
+        } else {
+            // Write should throw
+            for (int i = 0; i < n; i++) {
+                try {
+                    view.storeAt((byte)3, i);
+                    fail("Write access not prevented: " + spec);
+                } catch (PyException pye) {
+                    // Expect TypeError (not BufferError which getBuffer can raise)
+                    assertEquals(Py.TypeError, pye.type);
+                }
+            }
+        }
+    }
+
+    /** Test method for {@link org.python.core.PyBuffer#storeAt(byte, int[])}. */
+    @Test
+    public void testStoreAtNdim() {
+        announce("storeAt (n-dim)");
+        int[] index = new int[1];
+        int n = ref.length;
+        int[] exp = ref.ints.clone();
+        if (!spec.readonly) {
+            // Write modified test material into each location using storeAt()
+            for (int i = 0; i < n; i++) {
+                index[0] = i;
+                byte v = (byte)(exp[i] ^ 3); // twiddle some bits
+                view.storeAt(v, index);
+            }
+            // Compare each location with modified test data using intAt()
+            for (int i = 0; i < n; i++) {
+                index[0] = i;
+                assertEquals(exp[i] ^ 3, view.intAt(index));
+            }
+            if (spec.shape.length == 1) {
+                // Check 2D index throws
+                try {
+                    view.storeAt((byte)1, 0, 0);
+                    fail("Use of 2D index did not raise exception");
+                } catch (PyException pye) {
+                    // Expect BufferError
+                    // XXX ... but should it be TypeError here?
+                    assertEquals(Py.BufferError, pye.type);
+                }
+            }
+        } else {
+            // Write should throw
+            for (int i = 0; i < n; i++) {
+                index[0] = i;
+                try {
+                    view.storeAt((byte)3, index);
+                    fail("Write access not prevented: " + spec);
+                } catch (PyException pye) {
+                    // Expect TypeError (not BufferError which getBuffer can raise)
+                    assertEquals(Py.TypeError, pye.type);
+                }
+            }
+        }
+    }
+
+    /** Test method for {@link org.python.core.PyBuffer#copyTo(byte[], int)}. */
+    @Test
+    public void testCopyTo() {
+        final int OFFSET = 5;
+        announce("copyTo");
+        int n = ref.length;
+        // Try with zero offset
+        byte[] actual = new byte[n];
+        view.copyTo(actual, 0);
+        ByteBufferTestSupport.assertBytesEqual("copyTo() incorrect", ref.bytes, actual, 0);
+        // Try to middle of array
+        actual = new byte[n + 2 * OFFSET];
+        view.copyTo(actual, OFFSET);
+        ByteBufferTestSupport.assertBytesEqual("copyTo(offset) incorrect", ref.bytes, actual,
+                OFFSET);
+        assertEquals("data before destination", 0, actual[OFFSET - 1]);
+        assertEquals("data after destination", 0, actual[OFFSET + n]);
+    }
+
+    /** Test method for {@link org.python.core.PyBuffer#copyTo(int, byte[], int, int)}. */
+    @Test
+    public void testSliceCopyTo() {
+        announce("copyTo (from slice)");
+        final int OFFSET = 3;
+
+        int n = ref.length;
+        byte[] before = new byte[n + 2 * OFFSET];
+        final byte BLANK = 7;
+        Arrays.fill(before, BLANK);
+
+        // Try destination positions in actual[] of 0 and OFFSET
+        for (int destPos = 0; destPos <= OFFSET; destPos += OFFSET) {
+            // Try source positions in 0 and OFFSET
+            for (int srcIndex = 0; srcIndex <= OFFSET; srcIndex += OFFSET) {
+
+                // A variety of lengths from zero to (n-srcIndex)-ish
+                for (int length = 0; srcIndex + length <= n; length = 2 * length + 1) {
+                    doTestSliceCopyTo(srcIndex, before, destPos, length, n);
+                }
+
+                // 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;
+                    doTestSliceCopyTo(srcIndex, before, destPos, length, n);
+                }
+            }
+        }
+    }
+
+    /** Helper function for {@link #testSliceCopyTo()} */
+    private void doTestSliceCopyTo(int srcIndex, byte[] before, int destPos, int length, int n) {
+
+        if (verbosity > 1) {
+            System.out.printf("  copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", srcIndex, srcIndex
+                    + length, n, destPos, destPos + length, before.length);
+        }
+
+        // Test the method
+        byte[] dest = before.clone();
+        view.copyTo(srcIndex, dest, destPos, length);
+
+        // Check the write to dest contains a correctly positioned copy of the view (=ref.bytes)
+        byte[] viewBytes = PyBufferTestSupport.bytesFromByteAt(view);
+        ByteBufferTestSupport.checkReadCorrect(ref.bytes, viewBytes, dest, destPos, length, 1,
+                srcIndex, 1);
+
+    }
+
+    /** Test method for {@link org.python.core.PyBuffer#copyFrom(byte[], int, int, int)}. */
+    @Test
+    public void testCopyFrom() {
+        announce("copyFrom");
+        final int OFFSET = 3;
+        final int L = ref.length;
+
+        // Make some source material to copy from (longer since need to test at OFFSET too).
+        byte[] src = (new ByteMaterial(48, ref.length + OFFSET, 1)).bytes;
+
+        // Our test is against the underlying object of which the view may be a slice
+        TestSpec underlying = spec.getOriginal();
+        int start = spec.getStart();
+        int stride = spec.getStride();
+
+        // Try source positions in 0 and OFFSET
+        for (int srcPos = 0; srcPos <= OFFSET; srcPos += OFFSET) {
+
+            // Try destination positions in test object of 0 and OFFSET
+            for (int destIndex = 0; destIndex <= OFFSET; destIndex += OFFSET) {
+
+                // A variety of lengths from zero to (n-destIndex)-ish
+                for (int length = 0; destIndex + length <= L; length = 2 * length + 1) {
+                    doTestCopyFrom(src, srcPos, underlying, start, length, stride, destIndex);
+                }
+
+                // And from exactly n-destIndex down to zero-ish
+                for (int trim = 0; destIndex + trim <= L; trim = 2 * trim + 1) {
+                    int length = ref.length - destIndex - trim;
+                    doTestCopyFrom(src, srcPos, underlying, start, length, stride, destIndex);
+                }
+            }
+        }
+    }
+
+    /** Helper function for {@link #testCopyFrom()} */
+    private void doTestCopyFrom(byte[] src, int srcPos, TestSpec underlying, int start, int length,
+            int stride, int destIndex) {
+
+        if (verbosity > 1) {
+            System.out.printf("  copy src[%d:%d] (%d) to dst[%d:%d]\n", srcPos, srcPos + length,
+                    ref.length, destIndex, destIndex + length);
+        }
+
+        // Initialise the object (have to do each time)
+        createObjAndView();
+        PyBuffer underlyingView = obj.getBuffer(underlying.flags & ~PyBUF.WRITABLE);
+        byte[] before = bytesFromByteAt(underlyingView);
+
+        if (!spec.readonly) {
+            // This is the call we are testing (a write operation).
+            view.copyFrom(src, srcPos, destIndex, length);
+
+            // Our test is against the underlying object of which the view may be a slice
+            byte[] after = bytesFromByteAt(underlyingView);
+            int underlyingDestIndex = start + destIndex * stride;
+
+            // Test that the corresponding bytes of the underlying object match data copied in
+            ByteBufferTestSupport.checkWriteCorrect(before, after, src, srcPos, length, 1,
+                    underlyingDestIndex, stride);
+
+        } else {
+            // Should fail (write operation)
+            try {
+                view.copyFrom(src, srcPos, destIndex, length);
+                fail("Write access not prevented: " + spec);
+            } catch (PyException pye) {
+                // Expect TypeError only if the buffer was readonly
+                assertEquals(Py.TypeError, pye.type);
+            }
+        }
+    }
+
+    /** Test method for {@link org.python.core.PyBuffer#copyFrom(PyBuffer)}. */
+    @Test
+    public void testCopyFromPyBuffer() {
+        announce("copyFrom (PyBuffer)");
+
+        /*
+         * The test material (this time) presents a view that has n items of size i, that are spaced
+         * in the underlying buffer with stride s.
+         */
+        final int n = spec.ref.length;
+        final int p = spec.getStride();
+
+        // The material we copy to it should have these strides:
+        int[] srcStrides;
+        if (n < 2) {
+            srcStrides = new int[] {1};
+        } else if (p > 2 || p < -2) {
+            srcStrides = new int[] {1, p - 1, p, p + 1, -p + 1, -p, -p - 1};
+        } else if (p == 2 || p == -2) {
+            srcStrides = new int[] {1, 2, 3, -1, -2, -3};
+        } else { // ( s==1 || s==-1 )
+            srcStrides = new int[] {1, 2, -1, -2};
+        }
+
+        // Also need the maximum absolute value
+        int maxStride = 0;
+        for (int stride : srcStrides) {
+            if (stride > maxStride) {
+                maxStride = stride;
+            } else if (-stride > maxStride) {
+                maxStride = -stride;
+            }
+        }
+
+        // And these offsets to the lowest-indexed source item
+        int maxOffset = n + 1;
+        int[] srcOffsets = new int[] {0, (maxOffset + 1) / 3, maxOffset};
+
+        // Make the source material to copy from, big enough to accommodate n strides
+        int srcMaterialSize = n * maxStride + maxOffset;
+        ByteMaterial srcMaterial = new ByteMaterial(48, srcMaterialSize, 1);
+
+        /*
+         * Now we need a series of PyBuffer views on the source data, sliced and offset according to
+         * the offset and stride values we have enumerated. We'd like to use the same factory as the
+         * current test view (this.view), because copy from its own type might be optimised, and a
+         * different, bog-standard factory to test the general case.
+         */
+        ExporterFactory[] factories = {spec.factory, new SimpleExporterFactory()};
+
+        for (ExporterFactory factory : factories) {
+            /*
+             * We'll use the same apparatus to create the source buffer as we use to make the test
+             * cases. The specifications for them will all be derived from this one:
+             */
+            TestSpec original = new TestSpec(factory, srcMaterial);
+            /*
+             * Do this where the pattern of indices constituting src overlaps (or not) the pattern
+             * of view in challenging ways, including greater and smaller strides.
+             */
+            for (int stride : srcStrides) {
+                for (int offset : srcOffsets) {
+                    int start = (stride > 0) ? offset : srcMaterialSize - offset - 1;
+                    doTestCopyFrom(original, start, n, stride);
+                }
+            }
+        }
+
+    }
+
+    /** Helper function for {@link #testCopyFromPyBuffer()} */
+    private void doTestCopyFrom(TestSpec original, int start, int n, int stride) {
+
+        // Derive sliced test material from the original
+        TestSpec srcSpec = new SlicedTestSpec(original, 1, start, n, stride);
+        ObjectAndView pair = srcSpec.makePair();
+        PyBuffer src = pair.view;
+        byte[] srcBytes = srcSpec.ref.bytes;
+
+        // And for the test object
+        int s = spec.getStart();
+        int p = spec.getStride();
+        String srcName = pair.obj.getClass().getSimpleName();
+        if (verbosity > 1) {
+            int end = start + (n - 1) * stride + (stride > 0 ? 1 : -1);
+            int e = s + (n - 1) * p + (p > 0 ? 1 : -1);
+            System.out.printf("  copy from src[%d:%d:%d] %s(%d) to obj[%d:%d:%d]\n", //
+                    start, end, stride, srcName, n, //
+                    s, e, p);
+        }
+
+        // Initialise the destination object and view (have to do each time) from spec
+        createObjAndView();
+
+        // Our test is against the underlying object of which the view may be a slice
+        TestSpec underlying = spec.getOriginal();
+        PyBuffer underlyingView = obj.getBuffer(underlying.flags & ~PyBUF.WRITABLE);
+        byte[] before = bytesFromByteAt(underlyingView);
+
+        if (!spec.readonly) {
+            // This is the call we are testing (a write operation).
+            view.copyFrom(src);
+
+            // Our test is against the underlying object of which the view may be a slice
+            byte[] after = bytesFromByteAt(underlyingView);
+
+            // Test that the corresponding bytes of the underlying object match data copied in
+            ByteBufferTestSupport.checkWriteCorrect(before, after, srcBytes, 0, n, 1, s, p);
+
+        } else {
+            // Should fail (write operation)
+            try {
+                view.copyFrom(src);
+                fail("Write access not prevented: " + spec);
+            } catch (PyException pye) {
+                // Expect TypeError only if the buffer was readonly
+                assertEquals(Py.TypeError, pye.type);
+            }
+        }
+    }
+
+    /** Test method for {@link org.python.core.PyBuffer#copyFrom(PyBuffer)} when source is same. */
+    @Test
+    public void testCopyFromSelf() {
+        announce("copyFrom (self)");
+
+        // The test material (this time) presents a view of n bytes from a buffer of L bytes.
+        final int n = ref.length;
+        TestSpec original = spec.getOriginal();
+        if (spec.readonly || spec == original || n < 1) {
+            // We're only testing with sliced writable views
+            return;
+        }
+        final int p = spec.getStride();
+        final int L = original.ref.length;
+
+        /*
+         * We want to make another sliced view on the same test object, with the same number of
+         * items n, but different stride and/or offset. Strides above, equal to and below (if
+         * possible) the destination stride are of interest.
+         */
+        int[] srcStrides;
+        if (n < 2) {
+            srcStrides = new int[] {1};
+        } else if (p > 2 || p < -2) {
+            srcStrides = new int[] {1, p - 1, p, p + 1, -p + 1, -p, -p - 1};
+        } else if (p == 2 || p == -2) {
+            srcStrides = new int[] {1, 2, 3, -1, -2, -3};
+        } else { // ( p==1 || p==-1 )
+            srcStrides = new int[] {1, 2, -1, -2};
+        }
+
+        for (int srcStride : srcStrides) {
+            int absStride;
+            if (srcStride > 0) {
+                absStride = srcStride;
+                /*
+                 * Compute the highest start index such that we can fit n items spaced at absStride
+                 * into the buffer before reaching the end.
+                 */
+                int maxOffset = L - 1 - absStride * (n - 1);
+                // There might not be such an start. If there is, we can do one or more tests.
+                if (maxOffset >= 0) {
+                    // A positive-stepping slice could fit, for some start positions
+                    int incOffset = 1 + maxOffset / 4;
+                    for (int srcOffset = 0; srcOffset <= maxOffset; srcOffset += incOffset) {
+                        doTestCopyFromSelf(srcOffset, srcStride, n);
                     }
-
-                    // 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);
-                        }
+                }
+            } else {// srcStride < 0
+                absStride = -srcStride;
+                /*
+                 * Compute the lowest start index such that we can fit n items spaced at absStride
+                 * into the buffer before reaching the beginning.
+                 */
+                int minOffset = absStride * (n - 1) + 1;
+                // There might not be such an start. If there is, we can do one or more tests.
+                if (minOffset < L) {
+                    // A negative-stepping slice could fit, for some start positions
+                    int incOffset = 1 + (L - 1 - minOffset) / 4;
+                    for (int srcOffset = L - 1; srcOffset > minOffset; srcOffset -= incOffset) {
+                        doTestCopyFromSelf(srcOffset, srcStride, n);
                     }
                 }
             }
         }
     }
 
-    /** 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);
+    /** Helper function for {@link #testCopyFromPyBuffer()} */
+    private void doTestCopyFromSelf(int srcStart, int srcStride, int n) {
+
+        // Initialise the destination object and view (have to do each time) from spec
+        createObjAndView();
+
+        // Report the slice of the test object we are writing
+        int dstStart = spec.getStart();
+        int dstStride = spec.getStride();
+        String srcName = obj.getClass().getSimpleName();
+        if (verbosity > 1) {
+            int srcEnd = srcStart + (n - 1) * srcStride + (srcStride > 0 ? 1 : -1);
+            int dstEnd = dstStart + (n - 1) * dstStride + (dstStride > 0 ? 1 : -1);
+            System.out.printf("  copy from obj[%d:%d:%d] %s(%d) to obj[%d:%d:%d]\n", //
+                    srcStart, srcEnd, srcStride, srcName, n, //
+                    dstStart, dstEnd, dstStride);
         }
-        BufferTestPair pair = new BufferTestPair(exporter, material, readonly);
-        queue(pair);
-        return pair.view;
-    }
+        assert !spec.readonly;  // Test is only called if writable
 
-    /** 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) {
+        // Our test is against the underlying object of which the view may be a slice
+        try (PyBuffer underlying = obj.getBuffer(PyBUF.FULL_RO)) {
 
-        int flags = readonly ? PyBUF.FULL_RO : PyBUF.FULL;
-        PyBuffer subject = null;
+            // Take a snapshot before the call
+            byte[] before = bytesFromByteAt(underlying);
 
-        /*
-         * 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);
-            }
-        }
+            // Take the required slice-view to use as the source.
+            PyBuffer src = underlying.getBufferSlice(PyBUF.FULL_RO, srcStart, n, srcStride);
+            byte[] srcBytes = bytesFromByteAt(src);
 
-        return subject;
-    }
+            // This is the call we are testing (a write operation).
+            view.copyFrom(src);
 
-    /** Queue one instance of test material for a read-only or writable object. */
-    private void queue(BufferTestPair pair) {
-        buffersToRead.add(pair);
-        if (pair.readonly) {
-            buffersToFailToWrite.add(pair);
-        } else {
-            buffersToWrite.add(pair);
-        }
-    }
-
-    /** Read operations should succeed on all these objects. */
-    private List<BufferTestPair> buffersToRead = new LinkedList<BufferTestPair>();
-    /** Write operations should succeed on all these objects. */
-    private List<BufferTestPair> buffersToWrite = new LinkedList<BufferTestPair>();
-    /** Write operations should fail on all these objects. */
-    private List<BufferTestPair> buffersToFailToWrite = new LinkedList<BufferTestPair>();
-
-    /**
-     * 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 {@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};
-
-    /**
-     * A one-dimensional exporter with stride!=1 is restricted to give us a buffer only for these
-     * flag types.
-     */
-    private static final int[] strided1DFlags = {PyBUF.STRIDES, PyBUF.INDIRECT, PyBUF.FULL_RO};
-
-    /** 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) {
-            if (verbosity > 0) {
-                System.out.println("isReadonly: " + test);
-            }
-            assertFalse(test.view.isReadonly());
-        }
-
-        for (BufferTestPair test : buffersToFailToWrite) {
-            if (verbosity > 0) {
-                System.out.println("isReadonly: " + test);
-            }
-            assertTrue(test.view.isReadonly());
+            // Test that the corresponding bytes of the underlying object match data copied in
+            byte[] after = bytesFromByteAt(underlying);
+            ByteBufferTestSupport.checkWriteCorrect(before, after, srcBytes, 0, n, 1, dstStart,
+                    dstStride);
         }
     }
 
     /**
-     * Test method for {@link org.python.core.PyBUF#getNdim()}.
+     * Test method for {@link org.python.core.BufferProtocol#getBuffer()} and
+     * {@link org.python.core.PyBuffer#getBuffer()}.
      */
-    public void testGetNdim() {
-        for (BufferTestPair test : buffersToRead) {
-            if (verbosity > 0) {
-                System.out.println("getNdim: " + test);
-            }
-            assertEquals("unexpected ndim", test.shape.length, test.view.getNdim());
-        }
-    }
-
-    /**
-     * Test method for {@link org.python.core.PyBUF#getShape()}.
-     */
-    public void testGetShape() {
-        for (BufferTestPair test : buffersToRead) {
-            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);
-        }
-    }
-
-    /**
-     * Test method for {@link org.python.core.PyBUF#getLen()}.
-     */
-    public void testGetLen() {
-        for (BufferTestPair test : buffersToRead) {
-            if (verbosity > 0) {
-                System.out.println("getLen: " + test);
-            }
-            assertEquals("unexpected length", test.material.length, test.view.getLen());
-        }
-    }
-
-    /**
-     * Test method for {@link org.python.core.PyBuffer#byteAt(int)}.
-     */
-    public void testByteAt() {
-        for (BufferTestPair test : buffersToRead) {
-            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.view.byteAt(i));
-            }
-        }
-    }
-
-    /**
-     * Test method for {@link org.python.core.PyBuffer#byteAt(int[])}.
-     */
-    public void testByteAtNdim() {
-        int[] index = new int[1];
-        for (BufferTestPair test : buffersToRead) {
-            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 view
-            for (int i = 0; i < n; i++) {
-                index[0] = i;
-                assertEquals(exp[i], test.view.byteAt(index));
-            }
-
-            // Check 2D index throws
-            try {
-                test.view.byteAt(0, 0);
-                fail("Use of 2D index did not raise exception");
-            } catch (PyException pye) {
-                // Expect BufferError
-                assertEquals(Py.BufferError, pye.type);
-            }
-        }
-    }
-
-    /**
-     * Test method for {@link org.python.core.PyBuffer#intAt(int)}.
-     */
-    public void testIntAt() {
-        for (BufferTestPair test : buffersToRead) {
-            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.view.intAt(i));
-            }
-        }
-    }
-
-    /**
-     * Test method for {@link org.python.core.PyBuffer#intAt(int[])}.
-     */
-    public void testIntAtNdim() {
-        int[] index = new int[1];
-        for (BufferTestPair test : buffersToRead) {
-            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 view
-            for (int i = 0; i < n; i++) {
-                index[0] = i;
-                assertEquals(exp[i], test.view.intAt(index));
-            }
-            // Check 2D index throws
-            try {
-                test.view.intAt(0, 0);
-                fail("Use of 2D index did not raise exception");
-            } catch (PyException pye) {
-                // Expect BufferError
-                assertEquals(Py.BufferError, pye.type);
-            }
-        }
-    }
-
-    /**
-     * Test method for {@link org.python.core.PyBuffer#storeAt(byte, int)}.
-     */
-    public void testStoreAt() {
-        for (BufferTestPair test : buffersToWrite) {
-            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.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.view.intAt(i));
-            }
-        }
-    }
-
-    /**
-     * Test method for {@link org.python.core.PyBuffer#storeAt(byte, int[])}.
-     */
-    public void testStoreAtNdim() {
-        for (BufferTestPair test : buffersToWrite) {
-            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.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.view.intAt(i));
-            }
-            // Check 2D index throws
-            try {
-                test.view.storeAt((byte)1, 0, 0);
-                fail("Use of 2D index did not raise exception");
-            } catch (PyException pye) {
-                // Expect BufferError
-                assertEquals(Py.BufferError, pye.type);
-            }
-        }
-    }
-
-    /**
-     * Test method for {@link org.python.core.PyBuffer#copyTo(byte[], int)}.
-     */
-    public void testCopyTo() {
-        final int OFFSET = 5;
-        for (BufferTestPair test : buffersToRead) {
-            if (verbosity > 0) {
-                System.out.println("copyTo: " + test);
-            }
-            int n = test.material.length;
-            // Try with zero offset
-            byte[] actual = new byte[n];
-            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.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]);
-        }
-    }
-
-    /**
-     * Test method for {@link org.python.core.PyBuffer#copyTo(int, byte[], int, int)}.
-     */
-    public void testSliceCopyTo() {
-        final int OFFSET = 5;
-        final byte BLANK = 7;
-
-        for (BufferTestPair test : buffersToRead) {
-            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];
-
-            // Try destination positions in actual[] of 0 and OFFSET
-            for (int destPos = 0; destPos <= OFFSET; destPos += OFFSET) {
-                // Try source positions in 0 and OFFSET
-                for (int srcIndex = 0; srcIndex <= OFFSET; srcIndex += OFFSET) {
-
-                    // A variety of lengths from zero to (n-srcIndex)-ish
-                    for (int length = 0; srcIndex + length <= n; length = 2 * length + 1) {
-
-                        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
-                        view.copyTo(srcIndex, actual, destPos, length);
-
-                        // Check changed part of destination
-                        assertBytesEqual("copyTo(slice) incorrect", test.material.bytes, srcIndex,
-                                length, actual, destPos);
-                        if (destPos > 0) {
-                            assertEquals("data before destination", BLANK, actual[destPos - 1]);
-                        }
-                        assertEquals("data after destination", BLANK, actual[destPos + length]);
-                    }
-
-                    // 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;
-
-                        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
-                        view.copyTo(srcIndex, actual, destPos, length);
-
-                        // Check changed part of destination
-                        assertBytesEqual("copyTo(slice) incorrect", test.material.bytes, srcIndex,
-                                length, actual, destPos);
-                        if (destPos > 0) {
-                            assertEquals("data before destination", BLANK, actual[destPos - 1]);
-                        }
-                        assertEquals("data after destination", BLANK, actual[destPos + length]);
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Test method for {@link org.python.core.PyBuffer#copyFrom(byte[], int, int, int)}.
-     */
-    public void testCopyFrom() {
-        final int OFFSET = 5;
-        final byte BLANK = 7;
-
-        for (BufferTestPair test : buffersToWrite) {
-            if (verbosity > 0) {
-                System.out.println("copyFrom(): " + test);
-            }
-            PyBuffer view = test.view;
-
-            int n = test.material.length;
-            byte[] actual = new byte[n];
-            byte[] expected = new byte[n];
-
-            // Make some source material for copies (need to test at OFFSET too).
-            byte[] src = new byte[n + OFFSET];
-            for (int i = 0; i < src.length; i++) {
-                src[i] = (byte)i;
-            }
-
-            // Try destination positions in test object of 0 and OFFSET
-            for (int destIndex = 0; destIndex <= OFFSET; destIndex += OFFSET) {
-
-                // Try source positions in 0 and OFFSET
-                for (int srcPos = 0; srcPos <= OFFSET; srcPos += OFFSET) {
-
-                    // A variety of lengths from zero to (n-destIndex)-ish
-                    for (int length = 0; destIndex + length <= n; length = 2 * length + 1) {
-
-                        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++) {
-                            expected[i] = BLANK;
-                            view.storeAt(BLANK, i);
-                        }
-
-                        // Test the method and extract the result to actual[]
-                        view.copyFrom(src, srcPos, destIndex, length);
-                        view.copyTo(actual, 0);
-
-                        // Complete what is should be in expected[]
-                        for (int i = 0; i < length; i++) {
-                            expected[destIndex + i] = src[srcPos + i];
-                        }
-                        assertBytesEqual("copyFrom() incorrect", expected, actual, 0);
-                    }
-
-                    // And from exactly n-destIndex down to zero-ish
-                    for (int trim = 0; destIndex + trim <= n; trim = 2 * trim + 1) {
-                        int length = n - destIndex - trim;
-
-                        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++) {
-                            expected[i] = BLANK;
-                            view.storeAt(BLANK, i);
-                        }
-
-                        // Test the method and extract the result to actual[]
-                        view.copyFrom(src, srcPos, destIndex, length);
-                        view.copyTo(actual, 0);
-
-                        // Complete what is should be in expected[]
-                        for (int i = 0; i < length; i++) {
-                            expected[destIndex + i] = src[srcPos + i];
-                        }
-                        assertBytesEqual("copyFrom() incorrect", expected, actual, 0);
-                    }
-                }
+    public void testGetBufferForRead() {
+        announce("getBuffer(READ): ");
+        // Go through all the allowed combinations of flags and tassles
+        for (int flags : spec.validFlags) {
+            for (int tassles : spec.validTassles) {
+                PyBuffer view2 = view.getBuffer(flags | tassles);
+                assertNotNull(view2);
             }
         }
     }
@@ -639,161 +809,91 @@
      * 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);
+    public void testGetBufferForWrite() {
+        announce("getBuffer(WRITE): ");
+        if (!spec.readonly) {
+            // Go through all the allowed combinations of flags and tassles adding WRITABLE
+            for (int flags : spec.validFlags) {
+                for (int tassles : spec.validTassles) {
+                    PyBuffer view2 = view.getBuffer(flags | tassles | PyBUF.WRITABLE);
+                    assertNotNull(view2);
                 }
             }
-        }
-
-        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) {
+        } else {
+            // Go through all the allowed combinations of flags adding WRITABLE
+            for (int flags : spec.validFlags) {
                 try {
-                    test.subject.getBuffer(flags | PyBUF.WRITABLE);
-                    fail("Write access not prevented: " + test);
+                    view.getBuffer(flags | PyBUF.WRITABLE);
+                    fail("Write access not prevented: " + spec);
                 } 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.
+     * PyBuffer in the try-with-resources pattern.
      */
-    public void testRelease() {
+    @Test
+    public void testReleaseTryWithResources() {
+        announce("release (via try)");
+        /*
+         * this.obj is an actual exporter and this.view is a buffer view onto it.
+         */
+        int flags = PyBUF.STRIDES | PyBUF.FORMAT;
 
-        /*
-         * 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>();
-
-        // Test a balanced sequence of acquire and release using try-with-resources
-        for (BufferTestPair test : buffersToRead) {
-            doTestTryWithResources(test);
+        // The test setup should guarantee view is the only export
+        try (PyBuffer c = obj.getBuffer(flags)) { // = 2 exports
+            try (PyBuffer b = obj.getBuffer(PyBUF.FULL_RO); PyBuffer d = c.getBuffer(flags)) {
+                maybeCheckExporting(obj);// = 4 exports
+            }
+            maybeCheckExporting(obj); // = 2 exports
+            throw new Throwable("test");
+        } catch (Throwable e) {
+            // Meh
         }
-
-        // Now test a pattern of acquire and release with one more release than acquire
-        for (BufferTestPair test : buffersToRead) {
-            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);
-            }
-        }
-
+        maybeCheckExporting(obj); // = 1 export
+        view.release();
+        maybeCheckNotExporting(obj); // = 0 exports
     }
 
     /**
-     * Exercise try-with-resources on one BufferTestPair.
+     * Test method for {@link org.python.core.PyBUF#release()}, exercising release semantics in a
+     * sequence orchestrated by the client code. At the end, the view should be fully released, (
+     * {@link PyBuffer#isReleased()}<code>==true</code>).
      */
-    private void doTestTryWithResources(BufferTestPair test) {
+    @Test
+    public void testRelease() {
+        announce("release");
+        int flags = PyBUF.STRIDES | PyBUF.FORMAT;
 
-        if (verbosity > 0) {
-            System.out.println("try with resources: " + 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
-        try (PyBuffer c = sub.getBuffer(flags)) {   // = N+1 exports
-            try (PyBuffer b = sub.getBuffer(PyBUF.FULL_RO); PyBuffer d =c.getBuffer(flags)) {
-                checkExporting(sub);// = N+3 exports
-            }
-            checkExporting(sub);                    // = N+1 exports
-        }
-        checkExporting(sub);                        // = N export
-    }
-
-    /**
-     * 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);
+        // The object will be exporting view only
+        PyBuffer a = view; // = 1 exports
+        PyBuffer b = obj.getBuffer(PyBUF.FULL_RO); // = 2 export
+        PyBuffer c = obj.getBuffer(flags); // = 3 exports
+        maybeCheckExporting(obj);
 
         // Now see that releasing in some other order works correctly
-        b.release();                                // = N+2 exports
-        a.release();                                // = N+1 exports
-        checkExporting(sub);
+        b.release(); // = 2 exports
+        a.release(); // = 1 exports
+        maybeCheckExporting(obj);
 
         // 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());
+        PyBuffer d = c.getBuffer(flags); // = 2 exports
+        c.release(); // = 1 export
+        maybeCheckExporting(obj);
+        d.release(); // = no exports
 
         // Further releases are an error
         try {
-            view.release();                        // = -1 exports (oops)
+            view.release(); // = -1 exports (oops)
             fail("excess release not detected");
         } catch (Exception e) {
             // Success
         }
-
     }
 
     /**
@@ -802,41 +902,40 @@
      * 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) {
+    @Test
+    public void testGetAfterRelease() {
+        announce("getBuffer (after release)");
 
-        if (verbosity > 0) {
-            System.out.println("get again: " + test);
-        }
-        BufferProtocol sub = test.subject;
+        // The test objects should have exactly one export
+        view.release();
 
-        // Fail here if doTestRelease did not fully release, or
-        checkNotExporting(sub);
+        // The view can be checked, but not always the obj
+        maybeCheckNotExporting(obj);
+        maybeCheckNotExporting(view);
 
         // Further gets via the released buffer are an error
         try {
-            test.view.getBuffer(PyBUF.FULL_RO);
+            view.getBuffer(PyBUF.FULL_RO);
             fail("PyBuffer.getBuffer after final release not detected");
         } catch (Exception e) {
             // Detected *and* prevented?
-            checkNotExporting(sub);
+            maybeCheckNotExporting(obj);
         }
 
         // And so are sliced gets
         try {
-            test.view.getBufferSlice(PyBUF.FULL_RO, 0, 0);
+            view.getBufferSlice(PyBUF.FULL_RO, 0, 0);
             fail("PyBuffer.getBufferSlice after final release not detected");
         } catch (Exception e) {
             // Detected *and* prevented?
-            checkNotExporting(sub);
+            maybeCheckNotExporting(obj);
         }
 
-        /*
-         * 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);
+        // Even after some abuse, we can still get and release a buffer.
+        PyBuffer b = obj.getBuffer(PyBUF.FULL_RO); // = 1 export
+        maybeCheckExporting(obj);
+        b.release(); // = 0 exports
+        maybeCheckNotExporting(obj);
     }
 
     /**
@@ -845,7 +944,7 @@
      *
      * @param subject
      */
-    private void checkExporting(BufferProtocol subject) {
+    private void maybeCheckExporting(BufferProtocol subject) {
         if (subject instanceof TestableExporter) {
             assertTrue("exports not being counted", ((TestableExporter)subject).isExporting());
         } else if (subject instanceof PyBuffer) {
@@ -853,8 +952,8 @@
         } 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");
+                ((PyByteArray)subject).bytearray_append(Py.One); // Appends one zero byte
+                fail("bytearray_append with exports should fail");
             } catch (Exception e) {
                 // Success
             }
@@ -868,7 +967,7 @@
      *
      * @param subject
      */
-    private void checkNotExporting(BufferProtocol subject) {
+    private void maybeCheckNotExporting(BufferProtocol subject) {
         if (subject instanceof TestableExporter) {
             assertFalse("exports counted incorrectly", ((TestableExporter)subject).isExporting());
         } else if (subject instanceof PyBuffer) {
@@ -886,125 +985,123 @@
         // Other types cannot be checked
     }
 
-    /**
-     * Test method for {@link org.python.core.PyBuffer#getBufferSlice(int, int, int, int)}.
-     */
+    /** Test method for {@link org.python.core.PyBuffer#getBufferSlice(int, int, int, int)}. */
+    @Test
     public void testGetBufferSliceWithStride() {
+        announce("getBuffer (slice & stride)");
 
-        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 = ref.length;
+        int M = (N + 4) / 4; // At least one and about N/4
 
-            // 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) {
 
-            // 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) {
+                    doTestGetBufferSliceWithStride(start, 0, 1);
+                    doTestGetBufferSliceWithStride(start, 0, 2);
 
-                    if (length == 0) {
-                        checkSlice(view, material, start, 0, 1, readonly);
-                        checkSlice(view, material, start, 0, 2, readonly);
+                } else if (length == 1 && start < N) {
+                    doTestGetBufferSliceWithStride(start, 1, 1);
+                    doTestGetBufferSliceWithStride(start, 1, 2);
 
-                    } else if (length == 1 && start < N) {
-                        checkSlice(view, material, start, 1, 1, readonly);
-                        checkSlice(view, material, start, 1, 2, readonly);
+                } else if (start < N) {
 
-                    } 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) {
+                            doTestGetBufferSliceWithStride(start, length, step);
+                        }
+                    }
 
-                        // And for a range of step sizes
-                        for (int step : sliceSteps) {
-                            // Check this is a feasible slice
-                            if (start + (length - 1) * step < N) {
-                                checkSlice(view, 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) {
-                                checkSlice(view, 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) {
+                            doTestGetBufferSliceWithStride(start, length, -step);
                         }
                     }
                 }
             }
         }
-
     }
 
     /**
      * Helper for {@link #testGetBufferSliceWithStride()} that obtains one sliced buffer to
      * specification and checks it against the material.
      */
-    private void checkSlice(PyBuffer view, ByteMaterial material, int start, int length, int step,
-            boolean readonly) {
+    private void doTestGetBufferSliceWithStride(int first, int count, int step) {
 
-        int flags = readonly ? PyBUF.FULL_RO : PyBUF.FULL;
+        // view is a view matching ref.bytes. Make a reference value for a further slice.
+        TestSpec slicedSpec = new SlicedTestSpec(spec, spec.getItemsize(), first, count, step);
 
         if (verbosity > 1) {
-            System.out.printf("  checkSlice: start=%4d, length=%4d, step=%4d \n", start, length,
-                    step);
+            System.out.printf(
+                    "  slice first=%4d, count=%4d, step=%4d -> underlying start=%4d, stride=%4d\n",
+                    first, count, step, slicedSpec.getStart(), slicedSpec.getStride());
         }
-        byte[] expected = sliceBytes(material.bytes, start, length, step);
-        PyBuffer sliceView = view.getBufferSlice(flags, start, length, step);
 
-        byte[] result = bytesFromByteAt(sliceView);
-        assertBytesEqual("  testGetBufferSliceWithStride failure: ", expected, result);
+        // Now compute that further slice using the library under test (not makePair)
+        PyBuffer slicedView = view.getBufferSlice(spec.flags, first, count, step);
+        byte[] slice = PyBufferTestSupport.bytesFromByteAt(slicedView);
+
+        // Did we get the same as the reference material in the
+        ByteBufferTestSupport.assertBytesEqual("slice incorrect", slicedSpec.ref.bytes, slice);
     }
 
-    /**
-     * Test method for {@link org.python.core.PyBuffer#getBuf()}.
-     */
-    public void testGetBuf() {
-        for (BufferTestPair test : buffersToRead) {
-            if (verbosity > 0) {
-                System.out.println("getBuf: " + test);
-            }
-            int stride = test.strides[0];
+    /** Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer()}. */
+    @Test
+    public void testGetNIOByteBuffer() {
+        announce("getNIOByteBuffer");
+        int stride = spec.getStride();
+        ByteBuffer bb = view.getNIOByteBuffer();
+        ByteBufferTestSupport.assertBytesEqual("buffer does not match reference", ref.bytes, bb,
+                stride);
+        if (spec.readonly) {
+            assertTrue("ByteBuffer should be read-only", bb.isReadOnly());
+        } else {
+            assertFalse("ByteBuffer should not be read-only", bb.isReadOnly());
+        }
 
-            if (stride == 1) {
+    }
 
-                // The client should not have to support navigation with the strides array
-                int flags = test.readonly ? PyBUF.SIMPLE : PyBUF.SIMPLE + PyBUF.WRITABLE;
-                PyBuffer view = test.subject.getBuffer(flags);
-
-                PyBuffer.Pointer bp = view.getBuf();
-                assertBytesEqual("buffer does not match reference", test.material.bytes, bp);
-
-            } else {
-                // The client will have to navigate with the strides array
-                int flags = test.readonly ? PyBUF.STRIDED_RO : PyBUF.STRIDED;
-                PyBuffer view = test.subject.getBuffer(flags);
-
-                stride = view.getStrides()[0];  // Just possibly != test.strides when length<=1
-                PyBuffer.Pointer bp = view.getBuf();
-                assertBytesEqual("buffer does not match reference", test.material.bytes, bp, stride);
-            }
-
+    /** Test method for {@link org.python.core.PyBuffer#hasArray()}. */
+    @Test
+    public void testHasArray() {
+        announce("hasArray");
+        if (spec.hasArray) {
+            assertTrue("a backing array was expected", view.hasArray());
+        } else {
+            assertFalse("no backing array was expected", view.hasArray());
         }
     }
 
-    /**
-     * Test method for {@link org.python.core.PyBuffer#getPointer(int)}.
-     */
+    /** Test method for {@link org.python.core.PyBuffer#getBuf()}. */
+    @Test
+    @SuppressWarnings("deprecation")
+    public void testGetBuf() {
+        announce("getBuf");
+        if (spec.hasArray) {
+            int stride = spec.getStride();
+            PyBuffer.Pointer bp = view.getBuf();
+            assertBytesEqual("buffer does not match reference", ref.bytes, bp, stride);
+        }
+    }
+
+    /** Test method for {@link org.python.core.PyBuffer#getPointer(int)}. */
+    @Test
+    @SuppressWarnings("deprecation")
     public void testGetPointer() {
-        for (BufferTestPair test : buffersToRead) {
-            if (verbosity > 0) {
-                System.out.println("getPointer: " + test);
-            }
-            PyBuffer view = test.view;
-            int n = test.material.length, itemsize = view.getItemsize();
-            byte[] exp = new byte[itemsize], bytes = test.material.bytes;
+        announce("getPointer");
+        if (spec.hasArray) {
+            int itemsize = spec.getItemsize();
+            byte[] exp = new byte[itemsize], bytes = ref.bytes;
 
-            for (int i = 0; i < n; i++) {
+            // Try to get a pointer to an item at each byte location in the buffer
+            for (int i = 0; i <= ref.length - itemsize; i++) {
                 // Expected result is one item (allow for itemsize)
                 int p = i * itemsize;
                 for (int j = 0; j < itemsize; j++) {
@@ -1018,18 +1115,15 @@
         }
     }
 
-    /**
-     * Test method for {@link org.python.core.PyBuffer#getPointer(int[])}.
-     */
+    /** Test method for {@link org.python.core.PyBuffer#getPointer(int[])}. */
+    @Test
+    @SuppressWarnings("deprecation")
     public void testGetPointerNdim() {
         int[] index = new int[1];
-        for (BufferTestPair test : buffersToRead) {
-            if (verbosity > 0) {
-                System.out.println("getPointer(array): " + test);
-            }
-            PyBuffer view = test.view;
-            int n = test.material.length, itemsize = view.getItemsize();
-            byte[] exp = new byte[itemsize], bytes = test.material.bytes;
+        announce("getPointer(array)");
+        if (spec.hasArray) {
+            int n = ref.length, itemsize = view.getItemsize();
+            byte[] exp = new byte[itemsize], bytes = ref.bytes;
 
             for (int i = 0; i < n; i++) {
                 // Expected result is one item (allow for itemsize)
@@ -1055,118 +1149,127 @@
         }
     }
 
-    /**
-     * Test method for {@link org.python.core.PyBUF#getStrides()}.
-     */
+    /** Test method for {@link org.python.core.PyBUF#getStrides()}. */
+    @Test
     public void testGetStrides() {
-        for (BufferTestPair test : buffersToRead) {
-            if (verbosity > 0) {
-                System.out.println("getStrides: " + test);
-            }
-            for (int flags : test.validFlags) {
-                PyBuffer view = test.subject.getBuffer(flags);
-                // Strides array irrespective of the client flags ... (different from CPython)
-                int[] strides = view.getStrides();
-                assertNotNull("strides[] should always be provided", strides);
-
-                // The strides must have the expected value if length >1
-                if (test.material.bytes.length > 1) {
-                    assertIntsEqual("unexpected strides", test.strides, strides);
-                }
+        announce("getStrides");
+        for (int flags : spec.validFlags) {
+            PyBuffer view = this.view.getBuffer(flags);
+            // Strides array irrespective of the client flags ... (different from CPython)
+            int[] strides = view.getStrides();
+            assertNotNull("strides[] should always be provided", strides);
+            // The strides must have the expected value if length >1
+            if (ref.bytes.length > 1) {
+                assertIntsEqual("unexpected strides", spec.strides, strides);
             }
         }
     }
 
-    /**
-     * Test method for {@link org.python.core.PyBUF#getSuboffsets()}.
-     */
+    /** Test method for {@link org.python.core.PyBUF#getSuboffsets()}. */
+    @Test
     public void testGetSuboffsets() {
-        for (BufferTestPair test : buffersToRead) {
-            if (verbosity > 0) {
-                System.out.println("getSuboffsets: " + test);
-            }
-            // Null for all test material
-            assertNull(test.view.getSuboffsets());
-        }
+        announce("getSuboffsets");
+        // Null for all test material
+        assertNull(view.getSuboffsets());
+
     }
 
-    /**
-     * Test method for {@link org.python.core.PyBUF#isContiguous(char)}.
-     */
+    /** Test method for {@link org.python.core.PyBUF#isContiguous(char)}. */
+    @Test
     public void testIsContiguous() {
-        for (BufferTestPair test : buffersToRead) {
-            if (verbosity > 0) {
-                System.out.println("isContiguous: " + test);
-            }
-            // True for all test material and orders (since 1-dimensional)
-            for (String orderMsg : validOrders) {
-                char order = orderMsg.charAt(0);
-                assertTrue(orderMsg, test.view.isContiguous(order));
-            }
+        announce("isContiguous");
+        // All test material is 1-dimensional so it's fairly simple and same for all orders
+        int ndim = spec.shape[0], stride = spec.getStride(), itemsize = spec.getItemsize();
+        boolean contig = ndim < 2 || stride == itemsize;
+        for (String orderMsg : validOrders) {
+            char order = orderMsg.charAt(0);
+            assertEquals(orderMsg, view.isContiguous(order), contig);
         }
     }
 
     private static final String[] validOrders = {"C-contiguous test fail",
             "F-contiguous test fail", "Any-contiguous test fail"};
 
-    /**
-     * Test method for {@link org.python.core.PyBuffer#getFormat()}.
-     */
+    /** Test method for {@link org.python.core.PyBuffer#getFormat()}. */
+    @Test
     public void testGetFormat() {
-        for (BufferTestPair test : buffersToRead) {
-            if (verbosity > 0) {
-                System.out.println("getFormat: " + test);
-            }
-            for (int flags : test.validFlags) {
-                PyBuffer view = test.subject.getBuffer(flags);
-                // Format given irrespective of the client flags ... (different from CPython)
-                assertNotNull("format should always be provided", view.getFormat());
-                assertEquals("B", view.getFormat());
-                // And, we can ask for it explicitly ...
-                view = test.subject.getBuffer(flags | PyBUF.FORMAT);
-                assertEquals("B", view.getFormat());
-            }
+        announce("getFormat");
+        TestSpec spec = this.spec;
+
+        for (int flags : spec.validFlags) {
+            PyBuffer view = this.view.getBuffer(flags);
+            // Format given irrespective of the client flags ... (different from CPython)
+            assertNotNull("format should always be provided", view.getFormat());
+            assertEquals("B", view.getFormat());
+            // And, we can ask for it explicitly ...
+            view = this.view.getBuffer(flags | PyBUF.FORMAT);
+            assertEquals("B", view.getFormat());
         }
     }
 
-    /**
-     * Test method for {@link org.python.core.PyBUF#getItemsize()}.
-     */
+    /** Test method for {@link org.python.core.PyBUF#getItemsize()}. */
+    @Test
     public void testGetItemsize() {
-        for (BufferTestPair test : buffersToRead) {
-            if (verbosity > 0) {
-                System.out.println("getItemsize: " + test);
-            }
-            // Unity for all test material
-            assertEquals(1, test.view.getItemsize());
-        }
+        announce("getItemsize");
+        // Unity for all test material
+        assertEquals(1, view.getItemsize());
+    }
+
+    /** Test method for {@link org.python.core.PyBuffer#toString()}. */
+    @Test
+    public void testToString() {
+        announce("toString");
+        String r = view.toString();
+        assertEquals("buffer does not match reference", ref.string, r);
     }
 
     /**
-     * Test method for {@link org.python.core.PyBuffer#toString()}.
+     * Custom assert method comparing the bytes at a {@link PyBuffer.Pointer} to those in a byte
+     * array, when that <code>Pointer</code> is obtained from a contiguous <code>PyBuffer</code>.
+     * Let <code>bp[i]</code> denote <code>bp.storage[bp.offset+i]</code>, by analogy with a C
+     * pointer. It is required that <code>bp[k] == expected[k]</code>, for every index in
+     * <code>expected</code>. If not, a <code>fail()</code> is declared.
+     *
+     * @param message to issue on failure
+     * @param expected expected byte array
+     * @param bp result to test
      */
-    public void testToString() {
-        for (BufferTestPair test : buffersToRead) {
-            if (verbosity > 0) {
-                System.out.println("toString: " + test);
-            }
-            String r = test.view.toString();
-            assertEquals("buffer does not match reference", test.material.string, r);
-        }
+    @SuppressWarnings("deprecation")
+    private static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp) {
+        assertBytesEqual(message, expected, bp, 1);
+    }
+
+    /**
+     * Custom assert method comparing the bytes at a {@link PyBuffer.Pointer} to those in a byte
+     * array, when that <code>Pointer</code> is obtained from a striding <code>PyBuffer</code>. Let
+     * <code>bp[i]</code> denote <code>bp.storage[bp.offset+i]</code>, by analogy with a C pointer.
+     * It is required that <code>bp[k*stride] == expected[k]</code>, for every index <code>k</code>
+     * in <code>expected</code>. If not, a <code>fail()</code> is declared.
+     *
+     * @param message to issue on failure
+     * @param expected expected byte array
+     * @param bp result to test
+     * @param stride in the <code>bp.storage</code> array
+     */
+    @SuppressWarnings("deprecation")
+    private static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp,
+            int stride) {
+        ByteBufferTestSupport.assertBytesEqual(message, expected, 0, expected.length, bp.storage,
+                bp.offset, stride);
     }
 
     /*
-     * ------------------------------------------------------------------------------------------- A
-     * series of custom exporters to permit testing abstracted from the Jython interpreter. These
+     * --------------------------------------------------------------------------------------------
+     * A series of custom exporters to permit testing abstracted from the Jython interpreter. These
      * use the implementation classes in org.python.core.buffer in ways very similar to the
      * implementations of bytearray and str.
-     * -------------------------------------------------------------------------------------------
+     * --------------------------------------------------------------------------------------------
      */
     /**
-     * A class to act as an exporter that uses the SimpleReadonlyBuffer. The exporter exports a new
-     * PyBuffer object to each consumer (although each references the same internal storage) and it
-     * does not track their fate. You are most likely to use this approach with an exporting object
-     * that is immutable (or at least fixed in size).
+     * A class to act as an exporter that uses the SimpleBuffer. The exporter exports a new PyBuffer
+     * object to each consumer (although each references the same internal storage) and it does not
+     * track their fate. You are most likely to use this approach with an exporting object that is
+     * immutable (or at least fixed in size).
      */
     static class SimpleExporter implements BufferProtocol {
 
@@ -1183,7 +1286,17 @@
 
         @Override
         public PyBuffer getBuffer(int flags) {
-            return new SimpleBuffer(flags, storage);
+            return new SimpleBuffer(flags, this, storage);
+        }
+
+    }
+
+    /** A factory for SimpleBuffer objects used in genTestSpects and some tests. */
+    private static class SimpleExporterFactory extends ReadonlyExporterFactory {
+
+        @Override
+        public BufferProtocol make(ByteMaterial m) {
+            return new SimpleExporter(m.getBytes());
         }
 
     }
@@ -1196,7 +1309,8 @@
         protected Reference<BaseBuffer> export;
 
         /**
-         * Try to re-use existing exported buffer, or return null if can't.
+         * Try to re-use existing exported buffer, or return null if can't: modelled after the
+         * buffer re-use strategy in {@link PyByteArray}.
          */
         protected BaseBuffer getExistingBuffer(int flags) {
             BaseBuffer pybuf = null;
@@ -1261,7 +1375,7 @@
             BaseBuffer pybuf = getExistingBuffer(flags);
             if (pybuf == null) {
                 // No existing export we can re-use
-                pybuf = new SimpleStringBuffer(flags, storage);
+                pybuf = new SimpleStringBuffer(flags, this, storage);
                 // Hold a reference for possible re-use
                 export = new SoftReference<BaseBuffer>(pybuf);
             }
@@ -1278,7 +1392,7 @@
      * avoiding the cost of duplicate buffers. This is the case with PyByteArray, which prohibits
      * operations that would resize it, while there are outstanding exports.
      */
-    static class SimpleWritableExporter extends TestableExporter {
+    private static class SimpleWritableExporter extends TestableExporter {
 
         protected byte[] storage;
 
@@ -1297,7 +1411,7 @@
             BaseBuffer pybuf = getExistingBuffer(flags);
             if (pybuf == null) {
                 // No existing export we can re-use
-                pybuf = new SimpleWritableBuffer(flags, storage) {
+                pybuf = new SimpleWritableBuffer(flags, this, storage) {
 
                     @Override
                     protected void releaseAction() {
@@ -1313,471 +1427,120 @@
 
     }
 
+    /** A class to act as an exporter that uses the RollYourOwnArrayBuffer class. */
+    private static class RollYourOwnExporter extends TestableExporter {
+
+        protected byte[] storage;
+
+        public RollYourOwnExporter(byte[] storage) {
+            this.storage = storage;
+        }
+
+        @Override
+        public PyBuffer getBuffer(int flags) {
+            // If we have already exported a buffer it may still be available for re-use
+            BaseBuffer pybuf = getExistingBuffer(flags);
+            if (pybuf == null) {
+                // No existing export we can re-use
+                pybuf = new RollYourOwnArrayBuffer(flags, this, storage);
+                // Hold a reference for possible re-use
+                export = new WeakReference<BaseBuffer>(pybuf);
+            }
+            return pybuf;
+        }
+
+    }
+
     /**
-     * Class to hold test material representing the same sequence of values 0..255 in several
-     * different ways.
+     * Minimal extension of BaseBuffer in order to test the default implementations there. They're
+     * slow, so mostly we override them in the implementations BaseArrayBuffer and BaseNIOBuffer,
+     * but they still have to be correct. The class represents a one-dimensional, strided array of
+     * bytes, so it can represent a slice of itself.
      */
-    protected static class ByteMaterial {
+    private static class RollYourOwnArrayBuffer extends BaseBuffer {
 
-        final String string;
-        final byte[] bytes;
-        final int[] ints;
-        final int length;
+        final static int FEATURES = PyBUF.WRITABLE | PyBUF.AS_ARRAY;
 
-        /** Construct from String. */
-        public ByteMaterial(String s) {
-            string = s;
-            length = s.length();
-            bytes = new byte[length];
-            ints = new int[length];
-            for (int i = 0; i < length; i++) {
-                int x = s.charAt(i);
-                ints[i] = x;
-                bytes[i] = (byte)x;
+        final byte[] storage;
+        final PyBuffer root;
+
+        /**
+         * Create a buffer view of the entire array.
+         *
+         * @param flags consumer requirements
+         * @param obj exporting object (or <code>null</code>)
+         * @param storage byte array exported in its entirety
+         */
+        public RollYourOwnArrayBuffer(int flags, BufferProtocol obj, byte[] storage) {
+            this(flags, null /* =this */, obj, storage, 0, storage.length, 1);
+        }
+
+        /**
+         * Construct a slice of a one-dimensional byte array.
+         *
+         * @param flags consumer requirements
+         * @param root on which release must be called when this is released
+         * @param obj exporting object (or <code>null</code>)
+         * @param storage raw byte array containing exported data
+         * @param index0 index into storage of item[0]
+         * @param count number of items in the slice
+         * @param stride in between successive elements of the new PyBuffer
+         * @throws PyException (BufferError) when expectations do not correspond with the type
+         */
+        public RollYourOwnArrayBuffer(int flags, PyBuffer root, BufferProtocol obj, byte[] storage,
+                int index0, int count, int stride) throws IndexOutOfBoundsException,
+                NullPointerException, PyException {
+            // Client will need to navigate using shape and strides if this is a slice
+            super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : STRIDES), //
+                    index0, new int[] {count}, new int[] {stride});
+            this.storage = storage;
+            // Check the potential index range
+            if (count > 0) {
+                int end = index0 + (count - 1) * stride;
+                final int END = storage.length - 1;
+                if (index0 < 0 || index0 > END || end < 0 || end > END) {
+                    throw new IndexOutOfBoundsException();
+                }
+            }
+            // Check client is compatible
+            checkRequestFlags(flags);
+            // Get a lease on the root PyBuffer (read-only). Last in case a check above fails.
+            if (root == null) {
+                this.root = this;
+                this.obj = obj;
+            } else {
+                this.root = root.getBuffer(FULL_RO);
+                this.obj = root.getObj();
             }
         }
 
-        /** Construct from byte array. */
-        public ByteMaterial(byte[] b) {
-            length = b.length;
-            StringBuilder buf = new StringBuilder(length);
-            bytes = new byte[length];
-            ints = new int[length];
-            for (int i = 0; i < length; i++) {
-                int x = 0xff & b[i];
-                ints[i] = x;
-                bytes[i] = (byte)x;
-                buf.appendCodePoint(x);
-            }
-            string = buf.toString();
-        }
-
-        /** Construct from int array. */
-        public ByteMaterial(int[] a) {
-            length = a.length;
-            StringBuilder buf = new StringBuilder(length);
-            bytes = new byte[length];
-            ints = new int[length];
-            for (int i = 0; i < length; i++) {
-                int x = a[i];
-                ints[i] = x;
-                bytes[i] = (byte)x;
-                buf.appendCodePoint(x);
-            }
-            string = buf.toString();
-        }
-
-        /** Construct from pattern on values (used modulo 256). */
-        public ByteMaterial(int start, int count, int inc) {
-            length = count;
-            StringBuilder buf = new StringBuilder(length);
-            bytes = new byte[length];
-            ints = new int[length];
-            int x = start;
-            for (int i = 0; i < length; i++) {
-                ints[i] = x;
-                bytes[i] = (byte)x;
-                buf.appendCodePoint(x);
-                x = (x + inc) & 0xff;
-            }
-            string = buf.toString();
+        @Override
+        protected PyBuffer getRoot() {
+            return root;
         }
 
         @Override
-        public String toString() {
-            StringBuilder buf = new StringBuilder(100);
-            buf.append("byte[").append(length).append("]={ ");
-            for (int i = 0; i < length; i++) {
-                if (i > 0) {
-                    buf.append(", ");
-                }
-                if (i >= 5) {
-                    buf.append(" ...");
-                    break;
-                } else {
-                    buf.append(ints[i]);
-                }
-            }
-            buf.append(" }");
-            return buf.toString();
-        }
-
-        /**
-         * @return a copy of the bytes array (that the client is allowed to modify)
-         */
-        byte[] getBytes() {
-            return bytes.clone();
-        }
-
-        /**
-         * Create material equivalent to a slice. this will not be used to create an exporter, but
-         * rather to specify data equivalent to the export.
-         *
-         * @param start first index to include
-         * @param length number of indices
-         * @param stride between indices
-         * @return ByteMaterial in which the arrays are a slice of this one
-         */
-        ByteMaterial slice(int start, int length, int stride) {
-            return new ByteMaterial(sliceBytes(bytes, start, length, stride));
-        }
-    }
-
-    /**
-     * Create a byte array from the values of the PyBuffer obtained using
-     * {@link PyBuffer#byteAt(int)}, to a length obtained from {@link PyBuffer#getLen()}.
-     *
-     * @param v the buffer
-     * @return the byte array
-     */
-    static byte[] bytesFromByteAt(PyBuffer v) {
-        final int N = v.getLen();
-        byte[] a = new byte[N];
-        for (int i = 0; i < N; i++) {
-            a[i] = v.byteAt(i);
-        }
-        return a;
-    }
-
-    /**
-     * Create a byte array that is a strided copy of the one passed in. The specifications are
-     * assumed correct for the size of that array.
-     *
-     * @param b source array
-     * @param start first index to include
-     * @param length number of indices
-     * @param stride between indices
-     * @return slice of b
-     */
-    static byte[] sliceBytes(byte[] b, int start, int length, int stride) {
-        byte[] a = new byte[length];
-        for (int i = 0, j = start; i < length; i++, j += stride) {
-            a[i] = b[j];
-        }
-        return a;
-    }
-
-    /**
-     * Customised assert method comparing a buffer pointer to a byte array, usually the one from
-     * ByteMaterial.
-     *
-     * @param message to issue on failure
-     * @param expected expected byte array
-     * @param bp result to test
-     */
-    static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp) {
-        assertBytesEqual(message, expected, bp, 1);
-    }
-
-    /**
-     * Customised assert method comparing a buffer pointer to a byte array, usually the one from
-     * ByteMaterial.
-     *
-     * @param message to issue on failure
-     * @param expected expected byte array
-     * @param bp result to test
-     * @param stride in the storage array
-     */
-    static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp, int stride) {
-        assertBytesEqual(message, expected, 0, expected.length, bp.storage, bp.offset, stride);
-    }
-
-    /**
-     * Customised assert method comparing a buffer pointer to a byte array, usually the one from
-     * ByteMaterial.
-     *
-     * @param message to issue on failure
-     * @param expected expected byte array
-     * @param expectedStart where to start the comparison in expected
-     * @param n number of bytes to test
-     * @param bb result to test
-     * @param stride in the storage array
-     */
-    static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n,
-            PyBuffer.Pointer bp, int stride) {
-        assertBytesEqual(message, expected, expectedStart, n, bp.storage, bp.offset, stride);
-    }
-
-    /**
-     * Customised assert method comparing a byte arrays: values in the actual value must match all
-     * those in expected[], and they must be the same length.
-     *
-     * @param message to issue on failure
-     * @param expected expected byte array
-     * @param actual result to test
-     */
-    static void assertBytesEqual(String message, byte[] expected, byte[] actual) {
-        assertEquals(message + " (array size)", expected.length, actual.length);
-        assertBytesEqual(message, expected, 0, expected.length, actual, 0, 1);
-    }
-
-    /**
-     * Customised assert method comparing byte arrays: values in the actual value starting at
-     * actual[actualStart] must match all those in expected[], and there must be enough of them.
-     *
-     * @param message to issue on failure
-     * @param expected expected byte array
-     * @param actual result to test
-     * @param actualStart where to start the comparison in actual
-     */
-    static void assertBytesEqual(String message, byte[] expected, byte[] actual, int actualStart) {
-        assertBytesEqual(message, expected, 0, expected.length, actual, actualStart, 1);
-    }
-
-    /**
-     * Customised assert method comparing byte arrays: values starting at actual[actualStart] must
-     * those starting at expected[expectedStart], for a distance of n bytes.
-     *
-     * @param message to issue on failure
-     * @param expected expected byte array
-     * @param expectedStart where to start the comparison in expected
-     * @param n number of bytes to test
-     * @param actual result to test
-     * @param actualStart where to start the comparison in actual
-     */
-    static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n,
-            byte[] actual, int actualStart) {
-        assertBytesEqual(message, expected, expectedStart, n, actual, actualStart, 1);
-    }
-
-    /**
-     * Customised assert method comparing byte arrays: values starting at actual[actualStart] must
-     * those starting at expected[expectedStart], for a distance of n bytes.
-     *
-     * @param message to issue on failure
-     * @param expected expected byte array
-     * @param expectedStart where to start the comparison in expected
-     * @param n number of bytes to test
-     * @param actual result to test
-     * @param actualStart where to start the comparison in actual
-     * @param stride spacing of bytes in actual array
-     */
-    static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n,
-            byte[] actual, int actualStart, int stride) {
-
-        if (actualStart < 0) {
-            fail(message + " (start<0 in result)");
-
-        } else if (expectedStart < 0) {
-            fail(message + " (start<0 in expected result): bug in test?");
-
-        } else if (actualStart + (n - 1) * stride + 1 > actual.length) {
-            fail(message + " (result too short)");
-
-        } else if (expectedStart + n > expected.length) {
-            fail(message + " (expected result too short): bug in test?");
-
-        } else {
-            // Should be safe to compare the values
-            int i = actualStart, j, jLimit = expectedStart + n;
-            for (j = expectedStart; j < jLimit; j++) {
-                if (actual[i] != expected[j]) {
-                    break;
-                }
-                i += stride;
-            }
-
-            // If we stopped early, diagnose the problem
-            if (j < jLimit) {
-                System.out.println("  expected:"
-                        + Arrays.toString(Arrays.copyOfRange(expected, expectedStart, expectedStart
-                                + n)));
-                System.out
-                        .println("    actual:"
-                                + Arrays.toString(Arrays.copyOfRange(actual, actualStart,
-                                        actualStart + n)));
-                System.out.println("  _actual_:" + Arrays.toString(actual));
-                fail(message + " (byte at " + j + ")");
-            }
-        }
-    }
-
-    /**
-     * Customised assert method comparing a int arrays: values in the actual value starting at
-     * actual[offset] must match all those in expected[], and there must be enough of them.
-     *
-     * @param message to issue on failure
-     * @param expected expected array
-     * @param actual result to test
-     * @param offset where to start the comparison in actual
-     */
-    static void assertIntsEqual(String message, int[] expected, int[] actual, int offset) {
-        int n = expected.length;
-        if (offset < 0) {
-            fail(message + " (offset<0)");
-        } else if (offset + n > actual.length) {
-            fail(message + " (too short)");
-        } else {
-            // Should be safe to compare the values
-            int i = offset, j;
-            for (j = 0; j < n; j++) {
-                if (actual[i++] != expected[j]) {
-                    break;
-                }
-            }
-            if (j < n) {
-                System.out.println("  expected:" + Arrays.toString(expected));
-                System.out.println("    actual:" + Arrays.toString(actual));
-                fail(message + " (int at " + j + ")");
-            }
-        }
-    }
-
-    /**
-     * Customised assert method comparing a int arrays: int in the actual value must match all those
-     * in expected[], and there must be the same number of them.
-     *
-     * @param message to issue on failure
-     * @param expected expected array
-     * @param actual result to test
-     */
-    static void assertIntsEqual(String message, int[] expected, int[] actual) {
-        int n = expected.length;
-        assertEquals(message, n, actual.length);
-        // Should be safe to compare the values
-        int j;
-        for (j = 0; j < n; j++) {
-            if (actual[j] != expected[j]) {
-                break;
-            }
-        }
-        if (j < n) {
-            System.out.println("  expected:" + Arrays.toString(expected));
-            System.out.println("    actual:" + Arrays.toString(actual));
-            fail(message + " (int at " + j + ")");
-        }
-    }
-
-    /**
-     * Within a given test case (e.g. the test of one particular method) we run many data sets, and
-     * these are created by {@link PyBufferTest#setUp()} as instances of this class. The main
-     * contents of the BufferTestPair are the test subject and the material. The subject may be one
-     * of the base objects specified in <code>setUp()</code>, or it may itself be a
-     * <code>PyBuffer</code> onto one of these (often a sliced buffer). The material contains an
-     * array of bytes (and equivalent int array and String) that is the array of bytes equivalent to
-     * the subject.
-     */
-    private static class BufferTestPair {
-
-        /**
-         * An object (or PyBuffer) that is the subject of the test
-         */
-        final BufferProtocol subject;
-
-        /**
-         * Test material (a byte array and its value as several different types) that has a value
-         * equivalent to the subject of the test.
-         */
-        final ByteMaterial material;
-
-        /**
-         * As a convenience for the simple tests (which is most of them!) this element is guaranteed
-         * to be a PyBuffer: if {@link #subject} is a {@link PyBuffer}, this member is simply
-         * another reference to the <code>subject</code>. If <code>subject</code> is a real
-         * exporter, {@link #view} is a new view on the subject.
-         */
-        final PyBuffer view;
-
-        /** The base exporter is of a type that can only provide read-only views. */
-        final boolean readonly;
-
-        /**
-         * Flags that may be used in {@link BufferProtocol#getBuffer(int)} or
-         * {@link PyBuffer#getBufferSlice(int, int, int, int)}.
-         */
-        final int[] validFlags;
-
-        /**
-         * Modifier flags that may be used in {@link BufferProtocol#getBuffer(int)} or
-         * {@link PyBuffer#getBufferSlice(int, int, int, int)}.
-         */
-        final int[] validTassles;
-
-        static final int[] STRIDES_1D = {1};
-
-        /** The shape array that the subject should match (will be single element in present tests) */
-        int[] shape;
-
-        /** The shape array that the subject should match (will be single element in present tests) */
-        int[] strides;
-
-        /**
-         * A subject and its reference material, together with explicit shape and strides arrays
-         * expected.
-         *
-         * @param subject of the test
-         * @param material containing a Java byte array that a view of the subject should equal
-         * @param shape of the array, when testing in N-dimensions
-         * @param strides of the array, when testing sliced views
-         * @param readonly if true the base exporter can only provide read-only views
-         */
-        public BufferTestPair(BufferProtocol subject, ByteMaterial material, int[] shape,
-                int[] strides, boolean readonly, int[] validFlags, int[] validTassles) {
-            this.subject = subject;
-            this.material = new ByteMaterial(material.ints);    // Copy in case modified
-            this.shape = shape;
-            this.strides = strides;
-            this.readonly = readonly;
-            this.validFlags = validFlags;
-            this.validTassles = validTassles;
-
-            int flags = readonly ? PyBUF.FULL_RO : PyBUF.FULL;
-
-            if (subject instanceof PyBuffer) {
-                this.view = (PyBuffer)subject;
-            } else {
-                PyBuffer v = null;
-                try {
-                    // System.out.printf("BufferTestPair: length=%d, readonly=%s\n",
-                    // material.length, readonly);
-                    v = subject.getBuffer(flags);
-                } catch (Exception e) {
-                    /*
-                     * We ignore this case if we fail, because we are not testing buffer creation
-                     * here, but making buffers to be tested. We'll test buffer creation in
-                     * testGetBuffer.
-                     */
-                }
-                this.view = v;
-            }
-        }
-
-        /**
-         * Short constructor for contiguous arrays in one dimension.
-         *
-         * @param subject of the test
-         * @param material containing a Java byte array that a view of the subject should equal
-         * @param readonly if true the base exporter can only provide read-only views
-         */
-        public BufferTestPair(BufferProtocol subject, ByteMaterial material, boolean readonly) {
-            this(subject, material, new int[1], STRIDES_1D, readonly, simpleFlags, simpleTassles);
-            shape[0] = material.length;
-        }
-
-        /**
-         * Short constructor for strided arrays in one dimension.
-         *
-         * @param subject of the test
-         * @param material containing a Java byte array that a view of the subject should equal
-         * @param stride of the array, when testing sliced views
-         * @param readonly if true the base exporter can only provide read-only views
-         */
-        public BufferTestPair(PyBuffer subject, ByteMaterial material, int stride, boolean readonly) {
-            this(subject, material, new int[1], new int[1], readonly, strided1DFlags,
-                    strided1DTassles);
-            shape[0] = material.length;
-            strides[0] = stride;
+        public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {
+            int newStart = index0 + start * strides[0];
+            int newStride = strides[0] * stride;
+            return new RollYourOwnArrayBuffer(flags, root, null, storage, newStart, length,
+                    newStride);
         }
 
         @Override
-        public String toString() {
-            int offset = view.getBuf().offset;
-            String offsetSpec = offset > 0 ? "[0@(" + offset + "):" : "[:";
-            int stride = strides[0];
-            String sliceSpec = offsetSpec + shape[0] + (stride != 1 ? "*(" + stride + ")]" : "]");
-            return subject.getClass().getSimpleName() + sliceSpec + " ( " + material.toString()
-                    + " )";
+        public ByteBuffer getNIOByteBufferImpl() {
+            return ByteBuffer.wrap(storage);
         }
 
+        @Override
+        protected byte byteAtImpl(int byteIndex) {
+            return storage[byteIndex];
+        }
+
+        @Override
+        protected void storeAtImpl(byte value, int byteIndex) throws IndexOutOfBoundsException,
+                PyException {
+            storage[byteIndex] = value;
+        }
     }
 }
diff --git a/tests/java/org/python/core/PyBufferTestSupport.java b/tests/java/org/python/core/PyBufferTestSupport.java
new file mode 100644
--- /dev/null
+++ b/tests/java/org/python/core/PyBufferTestSupport.java
@@ -0,0 +1,544 @@
+package org.python.core;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.python.core.ByteBufferTestSupport.ByteMaterial;
+
+/**
+ * Supporting test fixtures for testing {@link PyBuffer} implementations, this class provides means
+ * to generate test specifications and organise them into a list. This object creates and holds
+ * factories for the multiple examples of the several implementation types the PyBufferTest needs,
+ * together with the configuration the factory and the test need.
+ */
+public class PyBufferTestSupport {
+
+    /** Control amount of output while generating material. */
+    protected int verbosity;
+
+    /** Lengths we will use if we can when slicing view */
+    private final int[] sliceLengths;
+
+    /** Step sizes we will use if we can when slicing view */
+    private final int[] sliceSteps;
+
+    /** List of test data configurations. */
+    private List<TestSpec> testSpecList = new LinkedList<TestSpec>();
+
+    /**
+     * Create an instance, and choose the number an variety of tests that each call to
+     * {@link #generate(BufferProtocol, ByteMaterial, boolean)} will produce.
+     *
+     * @param sliceLengths what length of slices to try to make from each original
+     * @param sliceSteps step sizes (strides) to try to use
+     */
+    PyBufferTestSupport(int[] sliceLengths, int[] sliceSteps) {
+        this(0, sliceLengths, sliceSteps);
+    }
+
+    /**
+     * Create an instance, and choose the number an variety of tests that each call to
+     * {@link #generate(BufferProtocol, ByteMaterial, boolean)} will produce.
+     *
+     * @param verbosity how much noise to make when generating test data
+     * @param sliceLengths what length of slices to try to make from each original
+     * @param sliceSteps step sizes (strides) to try to use
+     */
+    PyBufferTestSupport(int verbosity, int[] sliceLengths, int[] sliceSteps) {
+        this.verbosity = verbosity;
+        this.sliceLengths = sliceLengths;
+        this.sliceSteps = sliceSteps;
+    }
+
+    /**
+     * Add to the test queue a series of test specifications for a particular type of exporter and
+     * byte material, in various sliced versions. The first argument provides a factory able to
+     * construct a test object bearing the {@link BufferProtocol} interface, from the
+     * {@link ByteMaterial} also supplied. The first test specification queued is based directly on
+     * such construction. Construction takes place when {@link TestSpec#make()} is called during the
+     * test constructor.
+     * <p>
+     * The method goes on to create a series of specifications that when invoked in test
+     * initialisation will provide sliced views.
+     * <p>
+     * When the test runs, it will be given one test specification. Either:
+     * <ol>
+     * <li>the test is given the original root specification and makes a <code>PyBuffer</code> from
+     * it, by a call to {@link TestSpec#make()}, whose implementation creates a test subject of
+     * appropriate type, or</li>
+     * <li>the test is given a derived sliced specification and makes a buffer from it, by a call to
+     * {@link TestSpec#make()}, whose implementation slices a buffer provided by the original root
+     * specification.</li>
+     * </ol>
+     * The slices are made with a variety of argument combinations, filtered down to those that make
+     * sense for the size of the direct view. The reference value in the derived specification
+     * {@link TestSpec#ref} is computed independently of the test subject, from the slice
+     * specification and a the reference value in the root specification.
+     *
+     * @param original to specify a test and from which to generate other tests
+     */
+    void add(ExporterFactory factory, ByteMaterial material) {
+
+        // Add test using the specification passed as arguments
+        TestSpec original = new TestSpec(factory, material);
+        queue(original);
+
+        // Generate some slices from the material and this direct view
+        int N = original.ref.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(original, start, 0, 1);
+                    queue(original, start, 0, 2);
+
+                } else if (length == 1 && start < N) {
+                    queue(original, start, 1, 1);
+                    queue(original, start, 1, 2);
+
+                } 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(original, start, length, step);
+                        }
+                    }
+
+                    // Now use all the step sizes negatively
+                    for (int step : sliceSteps) {
+                        // Check this is a feasible slice
+                        if (start - (length - 1) * step >= 0) {
+                            queue(original, start, length, -step);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /** Generate and queue one test of non-slice type (if getting a buffer succeeds). */
+    private void queue(TestSpec spec) {
+        if (verbosity > 2) {
+            System.out.printf("queue non-slice: length=%d, readonly=%s\n", spec.ref.length,
+                    spec.readonly);
+        }
+        testSpecList.add(spec);
+    }
+
+    /** Generate and queue one test of slice type (if getting a buffer succeeds). */
+    private void queue(TestSpec original, int start, int length, int step) {
+        /*
+         * 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);
+            }
+            TestSpec spec = new SlicedTestSpec(original, 1, start, length, step);
+            testSpecList.add(spec);
+        } 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 a copy of the generated list of test data in a form suitable for test construction
+     * with a JUnit parameterised runner, which is as a collection of arrays of objects, where each
+     * array becomes the arguments to the test constructor. (@see org.junit.runners.Parameterized)
+     *
+     * @return generated list of test data
+     */
+    List<TestSpec[]> getTestData() {
+        List<TestSpec[]> r = new ArrayList<TestSpec[]>(testSpecList.size());
+        for (TestSpec spec : testSpecList) {
+            r.add(new TestSpec[] {spec});
+        }
+        return r;
+    }
+
+    /**
+     * Create a byte array from the values of the PyBuffer obtained using
+     * {@link PyBuffer#byteAt(int)}, to a length obtained from {@link PyBuffer#getLen()}.
+     *
+     * @param v the buffer
+     * @return the byte array
+     */
+    static byte[] bytesFromByteAt(PyBuffer v) {
+        final int N = v.getLen();
+        byte[] a = new byte[N];
+        for (int i = 0; i < N; i++) {
+            a[i] = v.byteAt(i);
+        }
+        return a;
+    }
+
+    /**
+     * Interface to a factory capable of making a {@link PyBuffer} exporter from
+     * {@link ByteMaterial}.
+     */
+    interface ExporterFactory {
+
+        /** Make fresh test object. */
+        BufferProtocol make(ByteMaterial m);
+
+        /** Whether the test object will be read-only. */
+        boolean isReadonly();
+
+        /** Whether the test object will be able to provide access as a byte array. */
+        boolean hasArray();
+    }
+
+    abstract static class ReadonlyExporterFactory implements ExporterFactory {
+
+        @Override
+        public boolean isReadonly() {
+            return true;
+        }
+
+        @Override
+        public boolean hasArray() {
+            return true;
+        }
+
+    };
+
+    abstract static class WritableExporterFactory implements ExporterFactory {
+
+        @Override
+        public boolean isReadonly() {
+            return false;
+        }
+
+        @Override
+        public boolean hasArray() {
+            return true;
+        }
+
+    };
+
+    /**
+     * Class holding reference data for a test and a factory method that will produce an object with
+     * interface {@link BufferProtocol} for use in tests. The class has one principal method
+     * {@link TestSpec#makePair()}, which must return an {@link ObjectAndView} where the view
+     * element is equal to the reference {@link TestSpec#ref}. During a JUnit test, the test
+     * constructor will be called with a particular instance of this class and will call
+     * <code>makePair()</code> one or more times to get fresh test material.
+     */
+    static class TestSpec {
+
+        /** Factory for test objects. */
+        final ExporterFactory factory;
+        /** The value of the associated test object. */
+        final ByteMaterial ref;
+        /** The associated <code>PyBuffer</code> should be read-only. */
+        final boolean readonly;
+        /** The associated <code>PyBuffer</code> should be accessible as a JVM array. */
+        final boolean hasArray;
+        /** Parent TestSpec, when this is a derived one, or null if it is an original. */
+        final TestSpec parent;
+        /** The value of shape array that the view should have that matches {@link #ref}. */
+        final int[] shape;
+        /** The value of strides array that the view should have that matches {@link #ref}. */
+        final int[] strides;
+
+        /** Either {@link PyBUF#FULL_RO} or {@link PyBUF#FULL} according to {@link #readonly}. */
+        final int flags;
+
+        /** Allowable basic flag combinations, such as {@link PyBUF#STRIDES}. */
+        final int[] validFlags;
+
+        /** Allowable additional flag combinations, such as {@link PyBUF#FORMAT} */
+        final int[] validTassles;
+
+        /**
+         * A one-dimensional exporter should be able to give us a buffer for all these flag types.
+         */
+        static final int[] simpleFlags = {PyBUF.SIMPLE, PyBUF.ND, PyBUF.STRIDES, PyBUF.INDIRECT,
+                PyBUF.FULL_RO};
+
+        /** To {@link #simpleFlags} we can add any of these */
+        static final int[] simpleTassles = {0, PyBUF.FORMAT, PyBUF.C_CONTIGUOUS,
+                PyBUF.F_CONTIGUOUS, PyBUF.ANY_CONTIGUOUS};
+
+        /**
+         * Construct a specification for a 1D contiguous byte-array based on the exporter factory
+         * and reference data supplied.
+         *
+         * @param factory makes exporters of the particular type
+         * @param ref the fill those exporters should have
+         */
+        TestSpec(ExporterFactory factory, ByteMaterial ref) {
+            this(null, factory, ref, new int[] {ref.length}, new int[] {1}, simpleFlags,
+                    simpleTassles);
+        }
+
+        /**
+         * Construct a specification for a 1D contiguous item-array based on the exporter factory,
+         * shape data and reference data supplied.
+         *
+         * @param parent of this test specification
+         * @param ref the fill those exporters should have (also determines the item size)
+         * @param shape array defining number and size of dimensions (as {@link PyBUF#getShape()}
+         * @param strides array defining addressing polynomial (as {@link PyBUF#getStrides()})
+         * @param validFlags allowable basic flag combinations usable with this specification
+         * @param validTassles allowable additional flag combinations
+         */
+        protected TestSpec(TestSpec parent, ByteMaterial ref, int[] shape, int[] strides,
+                int[] validFlags, int[] validTassles) {
+            this(parent, parent.getOriginal().factory, ref, shape, strides, validFlags,
+                    validTassles);
+        }
+
+        /**
+         * Construct a specification for a 1D contiguous item-array based on the exporter factory,
+         * shape data and reference data supplied.
+         *
+         * @param parent of this test specification
+         * @param factory makes exporters of the particular type, given <code>ref</code>
+         * @param ref the fill those exporters should have (also determines the item size)
+         * @param shape array defining number and size of dimensions (as {@link PyBUF#getShape()}
+         * @param strides array defining addressing polynomial (as {@link PyBUF#getStrides()})
+         * @param validFlags allowable basic flag combinations usable with this specification
+         * @param validTassles allowable additional flag combinations
+         */
+        protected TestSpec(TestSpec parent, ExporterFactory factory, ByteMaterial ref, int[] shape,
+                int[] strides, int[] validFlags, int[] validTassles) {
+            this.parent = parent;
+            this.factory = factory;
+            this.readonly = factory.isReadonly();
+            this.hasArray = factory.hasArray();
+            this.flags = (readonly ? PyBUF.FULL_RO : PyBUF.FULL) | (hasArray ? PyBUF.AS_ARRAY : 0);
+            this.ref = ref;
+            this.shape = shape;
+            this.strides = strides;
+            this.validFlags = validFlags;
+            this.validTassles = validTassles;
+        }
+
+        /** Return the parent of this specification (or null when it is an original). */
+        final TestSpec getParent() {
+            return parent;
+        }
+
+        /** This is an original specification (parent is null). */
+        final boolean isOriginal() {
+            return parent == null;
+        }
+
+        /** Return the original of this specification (ancestor with no parent). */
+        final TestSpec getOriginal() {
+            TestSpec p = this;
+            while (!p.isOriginal()) {
+                p = p.getParent();
+            }
+            return p;
+        }
+
+        /** Return the item size. */
+        int getItemsize() {
+            return 1;
+        }
+
+        /** Return the stride that a buffer made from this specification should have. */
+        int getStride() {
+            return strides[0];
+        }
+
+        /** Return the start index that a buffer made from this specification should have. */
+        int getStart() {
+            return 0;
+        }
+
+        /** Simple holder class for a buffer exporter object and a related buffer. */
+        static class ObjectAndView {
+
+            final BufferProtocol obj;
+            final PyBuffer view;
+
+            ObjectAndView(BufferProtocol obj, PyBuffer view) {
+                this.obj = obj;
+                this.view = view;
+            }
+        }
+
+        /**
+         * Make the test object which must implement <code>BufferProtocol</code> and its
+         * <code>PyBuffer</code> view. The value as a byte array must equal {@link #ref}.
+         */
+        public ObjectAndView makePair() {
+            BufferProtocol obj = factory.make(ref);
+            PyBuffer view = obj.getBuffer(flags);
+            return new ObjectAndView(obj, view);
+        }
+
+        @SuppressWarnings("deprecation")
+        @Override
+        public String toString() {
+
+            ObjectAndView pair = makePair();
+            BufferProtocol obj = pair.obj;
+            PyBuffer view = pair.view;
+
+            StringBuilder sb = new StringBuilder(100);
+            sb.append(obj.getClass().getSimpleName()).append('[');
+
+            int offset, stride = getStride();
+
+            if (view.hasArray()) {
+                offset = view.getBuf().offset;
+            } else {
+                offset = view.getNIOByteBuffer().position();
+            }
+
+            if (offset > 0) {
+                sb.append(offset);
+            }
+
+            String plus = offset == 0 ? "" : "+";
+
+            if (stride == 1) {
+                sb.append(plus).append("k]");
+            } else if (stride == -1) {
+                sb.append("-k]");
+            } else if (stride < 0) {
+                sb.append("-").append(-stride).append("*k]");
+            } else {
+                /* stride>1 or ==0) */sb.append(plus).append(stride).append("*k]");
+            }
+
+            while (sb.length() < 30) {
+                sb.append(' ');
+            }
+            sb.append(view.isReadonly()?"R ":"W ");
+            sb.append("ref = ").append(ref.toString());
+
+            return sb.toString();
+        }
+    }
+
+    /**
+     * A test specification that is derived from a parent test specification, but will construct
+     * views sliced a particular way. In order to construct a test object, the factory of the parent
+     * is used, so that objects returned from here have the same type and root buffer value as the
+     * parent. However, {@link SlicedTestSpec#makePair()} returns a sliced view with the base
+     * exporter, and the reference material here is sliced correspondingly.
+     */
+    static class SlicedTestSpec extends TestSpec {
+
+        /** Number of consecutive bytes forming one item */
+        final int itemsize;
+        /** Index in the parent object of item 0 of this slice */
+        final int first;
+        /** The number of items that make up the slice. */
+        final int count;
+        /** The item-index distance in the parent from one item to the next of this slice. */
+        final int step;
+
+        /** Byte-index in the original byte-array object of byte 0 of item 0 of the slice */
+        final int start;
+
+        /**
+         * A one-dimensional exporter with stride!=1 is restricted to give us a buffer only for
+         * these flag types.
+         */
+        static final int[] strided1DFlags = {PyBUF.STRIDES, PyBUF.INDIRECT, PyBUF.FULL_RO};
+
+        /** To {@link #strided1DFlags} we can add any of these */
+        static final int[] strided1DTassles = {0, PyBUF.FORMAT};
+
+        /**
+         * Construct a test specification based on a parent, but yielding objects and reference
+         * material whose values are related to those of the parent according to the slice
+         * specification.
+         *
+         * @param parent specification of byte buffer to slice
+         * @param itemsize number of consecutive bytes forming one item
+         * @param first byte-index in the parent of byte 0 of item 0 the result
+         * @param count number of items in the slice
+         * @param step byte-index increment in the parent between items
+         */
+        SlicedTestSpec(TestSpec parent, int itemsize, int first, int count, int step) {
+            super(parent, parent.ref.slice(itemsize, first, count, step), new int[] {count},
+                    new int[1], strided1DFlags, strided1DTassles);
+            // It only seems to make sense for byte-array parent (or does all scale?)
+            if (parent.getItemsize() != 1) {
+                throw new IllegalArgumentException("Only byte-array parent supported");
+            }
+            this.itemsize = itemsize;
+            // Write these down verbatim for subsequent call to getBufferSlice
+            this.first = first;
+            this.count = count;
+            this.step = step;
+            // But these must be calculated carefully
+            this.start = parent.getStart() + first * parent.getStride();
+            this.strides[0] = step * parent.getStride();
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * The size given in construction of a <code>SlicedTestSpec</code>.
+         */
+        @Override
+        int getItemsize() {
+            return itemsize;
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * The start given in construction of a <code>SlicedTestSpec</code> is a start byte index
+         * specification, which could itself be striding on the underlying object's storage.
+         */
+        @Override
+        int getStart() {
+            return start;
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * In <code>SlicedTestSpec</code> the returned pair are a new instance of the root object
+         * (to be the original exporter) created by
+         *
+         * <pre>
+         * pair = parent.makePair();
+         * obj = pair.obj;
+         * </pre>
+         * and a <i>sliced</i> buffer view onto it created by
+         *
+         * <pre>
+         * view = pair.view.getBufferSlice(flags, first, count, step);
+         * </pre>
+         * This view-slicing will apply recursively if the parent is a {@link SlicedTestSpec}, just
+         * as the slicing of reference material was iterated in construction.
+         */
+        @Override
+        public ObjectAndView makePair() {
+            // Create a fresh test object and buffer view from the parent spec
+            ObjectAndView pair = parent.makePair();
+            // Make a sliced view and release the parent
+            PyBuffer view = pair.view.getBufferSlice(flags, first, count, step);
+            // Controlled release of the parent buffer since pair is local
+            pair.view.release();
+            return new ObjectAndView(pair.obj, view);
+        }
+
+    }
+}

-- 
Repository URL: https://hg.python.org/jython


More information about the Jython-checkins mailing list