From jython-checkins at python.org Sun Nov 4 15:37:30 2012 From: jython-checkins at python.org (alan.kennedy) Date: Sun, 4 Nov 2012 15:37:30 +0100 (CET) Subject: [Jython-checkins] =?utf-8?q?jython_=282=2E5=29=3A_Fixing_issue_17?= =?utf-8?q?54=3A_changing_the_implementation_of_the_modjy_sgi=2Einput_obje?= =?utf-8?q?ct?= Message-ID: <3XvfkL2wNBzRFj@mail.python.org> http://hg.python.org/jython/rev/4c3155645812 changeset: 6884:4c3155645812 branch: 2.5 parent: 6875:e160ef3d208f user: Alan Kennedy date: Sun Nov 04 14:31:11 2012 +0000 summary: Fixing issue 1754: changing the implementation of the modjy sgi.input object files: Lib/modjy/modjy_input.py | 167 ++++++++++ Lib/modjy/modjy_wsgi.py | 5 +- tests/modjy/java/com/xhaus/modjy/ModjyTestPostData.java | 4 +- tests/modjy/java/com/xhaus/modjy/ModjyTestWSGIStreams.java | 47 ++- tests/modjy/test_apps_dir/stream_tests.py | 8 + 5 files changed, 223 insertions(+), 8 deletions(-) diff --git a/Lib/modjy/modjy_input.py b/Lib/modjy/modjy_input.py new file mode 100644 --- /dev/null +++ b/Lib/modjy/modjy_input.py @@ -0,0 +1,167 @@ +### +# +# Copyright Alan Kennedy. +# +# You may contact the copyright holder at this uri: +# +# http://www.xhaus.com/contact/modjy +# +# The licence under which this code is released is the Apache License v2.0. +# +# The terms and conditions of this license are listed in a file contained +# in the distribution that also contained this file, under the name +# LICENSE.txt. +# +# You may also read a copy of the license at the following web address. +# +# http://modjy.xhaus.com/LICENSE.txt +# +### + +# +# This code adapted from the socket._fileobject class +# + +import jarray + +class modjy_input_object(object): + + def __init__(self, servlet_inputstream, bufsize=8192): + self.istream = servlet_inputstream + self.buffer_size = bufsize + self.buffer = "" + + def istream_read(self, n): + data = jarray.zeros(n, 'b') + m = self.istream.read(data) + if m == -1: # indicates EOF has been reached, so we just return the empty string + return "" + elif m <= 0: + return "" + if m < n: + data = data[:m] + return data.tostring() + + def read(self, size=-1): + data = self.buffer + if size < 0: + # Read until EOF + buffers = [] + if data: + buffers.append(data) + self.buffer = "" + recv_size = self.buffer_size + while True: + data = self.istream_read(recv_size) + if not data: + break + buffers.append(data) + return "".join(buffers) + else: + # Read until size bytes or EOF seen, whichever comes first + buf_len = len(data) + if buf_len >= size: + self.buffer = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self.buffer = "" + while True: + left = size - buf_len + recv_size = max(self.buffer_size, left) + data = self.istream_read(recv_size) + if not data: + break + buffers.append(data) + n = len(data) + if n >= left: + self.buffer = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return "".join(buffers) + + def readline(self, size=-1): + data = self.buffer + if size < 0: + # Read until \n or EOF, whichever comes first + nl = data.find('\n') + if nl >= 0: + nl += 1 + self.buffer = data[nl:] + return data[:nl] + buffers = [] + if data: + buffers.append(data) + self.buffer = "" + while True: + data = self.istream_read(self.buffer_size) + if not data: + break + buffers.append(data) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self.buffer = data[nl:] + buffers[-1] = data[:nl] + break + return "".join(buffers) + else: + # Read until size bytes or \n or EOF seen, whichever comes first + nl = data.find('\n', 0, size) + if nl >= 0: + nl += 1 + self.buffer = data[nl:] + return data[:nl] + buf_len = len(data) + if buf_len >= size: + self.buffer = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self.buffer = "" + while True: + data = self.istream_read(self.buffer_size) + if not data: + break + buffers.append(data) + left = size - buf_len + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + self.buffer = data[nl:] + buffers[-1] = data[:nl] + break + n = len(data) + if n >= left: + self.buffer = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return "".join(buffers) + + def readlines(self, sizehint=0): + total = 0 + list = [] + while True: + line = self.readline() + if not line: + break + list.append(line) + total += len(line) + if sizehint and total >= sizehint: + break + return list + + # Iterator protocols + + def __iter__(self): + return self + + def next(self): + line = self.readline() + if not line: + raise StopIteration + return line diff --git a/Lib/modjy/modjy_wsgi.py b/Lib/modjy/modjy_wsgi.py --- a/Lib/modjy/modjy_wsgi.py +++ b/Lib/modjy/modjy_wsgi.py @@ -28,6 +28,7 @@ create_py_file = PyFile from modjy_exceptions import * +from modjy_input import modjy_input_object server_name = "modjy" server_param_prefix = "%s.param" % server_name @@ -125,8 +126,8 @@ def set_wsgi_streams(self, req, resp, dict): try: - dict["wsgi.input"] = create_py_file(req.getInputStream(), "rb") - dict["wsgi.errors"] = create_py_file(System.err) + dict["wsgi.input"] = modjy_input_object(req.getInputStream()) + dict["wsgi.errors"] = create_py_file(System.err) except IOException, iox: raise ModjyIOException(iox) diff --git a/tests/modjy/java/com/xhaus/modjy/ModjyTestPostData.java b/tests/modjy/java/com/xhaus/modjy/ModjyTestPostData.java --- a/tests/modjy/java/com/xhaus/modjy/ModjyTestPostData.java +++ b/tests/modjy/java/com/xhaus/modjy/ModjyTestPostData.java @@ -27,7 +27,7 @@ setAppFile("post_data_tests.py"); } - public void doHeaderTest(String appName, String postData) throws Exception { + public void doPostTest(String appName, String postData) throws Exception { postDataTestSetUp(); setMethod("POST"); setAppName(appName); @@ -40,7 +40,7 @@ public void testPostDataLineEndsNotTranslated() throws Exception { String testData = "this\r\ndata\r\ncontains\r\ncarriage\r\nreturns\r\n"; String expectedData = "'"+testData.replace("\r", "\\r").replace("\n", "\\n")+"'"; - doHeaderTest("test_return_post_data", testData); + doPostTest("test_return_post_data", testData); assertEquals("Wrong post data returned >>" + getOutput() + "<< != >>"+expectedData+"<<", expectedData, getOutput()); } diff --git a/tests/modjy/java/com/xhaus/modjy/ModjyTestWSGIStreams.java b/tests/modjy/java/com/xhaus/modjy/ModjyTestWSGIStreams.java --- a/tests/modjy/java/com/xhaus/modjy/ModjyTestWSGIStreams.java +++ b/tests/modjy/java/com/xhaus/modjy/ModjyTestWSGIStreams.java @@ -63,6 +63,14 @@ doInputTest(appName, bodyContent, expectedContent, expectedLength, 0); } + protected String buildStringWithContents(int count, String contents) { + StringBuilder builder = new StringBuilder(); + for (int i = 0 ; i < count ; i++) { + builder.append(contents); + }; + return builder.toString(); + } + public void testEmptyInput() throws Exception { doInputTest("test_read_input_stream", "", "", 0); } @@ -81,6 +89,14 @@ } } + public void testAsciiInputWithReadSizeLargeInput() throws Exception { + String one_k = buildStringWithContents(1024, "X"); + String testData = buildStringWithContents(64, one_k); + for (int i = 64; i <= 65536; i=i*2) { + doInputTest("test_read_input_stream", testData, testData, testData.length(), i); + } + } + public void testAsciiInputReadline() throws Exception { doInputTest("test_readline_input_stream", "Hello\nWorld!\n", "Hello\n", 6); doInputTest("test_readline_input_stream", "Hello", "Hello", 5); @@ -88,17 +104,40 @@ public void testAsciiInputReadlineWithSize() throws Exception { // Let's test this: although PEP-333 says it's not supported, modjy can do it - doInputTest("test_readline_input_stream", "Hello\nWorld!\n", "Hello", 5, 5); + doInputTest("test_readline_input_stream", "Hello\nWorld!\n", "Hello\n", 6, 6); + } + + public void testAsciiInputReadlineWithSizeLessThanOneLine() throws Exception { + // Test where the size given is too small to actually reach a line end and + // a truncated line should be returned. + doInputTest("test_readline_input_stream", "Hello\nWorld!\n", "Hel", 3, 3); + } + + public void testAsciiInputReadlineWithSizeLongerThanOneLine() throws Exception { + // Test where the size given is long enough to take in a whole line + // and part of the next line. + doInputTest("test_readline_input_stream", "Hello\nWorld!\n", "Hello\n", 6, 10); + } + + public void testAsciiInputReadlineWithSizeLongerThanTwoLines() throws Exception { + // Test where the size given is long enough to take in a whole line + // and part of the next line. + doInputTest("test_readline_input_stream", "Hello\nWorld!\n", "Hello\n", 6, 32); } public void testAsciiInputWithReadlines() throws Exception { + doInputTest("test_readlines_input_stream", "Hello", "Hello", 5); + doInputTest("test_readlines_input_stream", "Hello\n", "Hello\n", 6); doInputTest("test_readlines_input_stream", "Hello\nWorld!\n", "Hello\n$World!\n", 14); - doInputTest("test_readlines_input_stream", "Hello", "Hello", 5); } public void testAsciiInputWithReadlinesWithHint() throws Exception { - // Let's leave this for now - // doInputTest("test_readlines_input_stream", "Hello\nWorld!\n", "Hello\n", 6, 5); + doInputTest("test_readlines_input_stream", "Hello\nWorld!\n", "Hello\n", 6, 5); + doInputTest("test_readlines_input_stream", "Hello\nWorld!\n", "Hello\n$World!\n", 14, 32); + } + + public void testInputIterator() throws Exception { + doInputTest("test_iter_input_stream", "Hello\nWorld!\n", "Hello\n$World!\n", 14); } public void testError() throws Exception { diff --git a/tests/modjy/test_apps_dir/stream_tests.py b/tests/modjy/test_apps_dir/stream_tests.py --- a/tests/modjy/test_apps_dir/stream_tests.py +++ b/tests/modjy/test_apps_dir/stream_tests.py @@ -83,6 +83,14 @@ writer(repr(output_dict)) return [] +def test_iter_input_stream(environ, start_response): + writer = start_response("200 OK", []) + wsgi_input = environ['wsgi.input'] + data = "$".join([line for line in wsgi_input]) + output_dict = {'data': data} + writer(repr(output_dict)) + return [] + def test_error_stream(environ, start_response): writer = start_response("200 OK", []) wsgi_errors = environ['wsgi.errors'] -- Repository URL: http://hg.python.org/jython From jython-checkins at python.org Sun Nov 4 15:37:31 2012 From: jython-checkins at python.org (alan.kennedy) Date: Sun, 4 Nov 2012 15:37:31 +0100 (CET) Subject: [Jython-checkins] =?utf-8?q?jython_=28merge_2=2E5_-=3E_default=29?= =?utf-8?q?=3A_merge_w/2=2E5=3A_Fixing_issue_1754=3A_changing_the_implemen?= =?utf-8?q?tation_of_the_modjy?= Message-ID: <3XvfkM6JKBzRFj@mail.python.org> http://hg.python.org/jython/rev/7ebb51401f54 changeset: 6885:7ebb51401f54 parent: 6883:4b061dc82e97 parent: 6884:4c3155645812 user: Alan Kennedy date: Sun Nov 04 14:32:26 2012 +0000 summary: merge w/2.5: Fixing issue 1754: changing the implementation of the modjy sgi.input object files: Lib/modjy/modjy_input.py | 167 ++++++++++ Lib/modjy/modjy_wsgi.py | 5 +- tests/modjy/java/com/xhaus/modjy/ModjyTestPostData.java | 4 +- tests/modjy/java/com/xhaus/modjy/ModjyTestWSGIStreams.java | 47 ++- tests/modjy/test_apps_dir/stream_tests.py | 8 + 5 files changed, 223 insertions(+), 8 deletions(-) diff --git a/Lib/modjy/modjy_input.py b/Lib/modjy/modjy_input.py new file mode 100644 --- /dev/null +++ b/Lib/modjy/modjy_input.py @@ -0,0 +1,167 @@ +### +# +# Copyright Alan Kennedy. +# +# You may contact the copyright holder at this uri: +# +# http://www.xhaus.com/contact/modjy +# +# The licence under which this code is released is the Apache License v2.0. +# +# The terms and conditions of this license are listed in a file contained +# in the distribution that also contained this file, under the name +# LICENSE.txt. +# +# You may also read a copy of the license at the following web address. +# +# http://modjy.xhaus.com/LICENSE.txt +# +### + +# +# This code adapted from the socket._fileobject class +# + +import jarray + +class modjy_input_object(object): + + def __init__(self, servlet_inputstream, bufsize=8192): + self.istream = servlet_inputstream + self.buffer_size = bufsize + self.buffer = "" + + def istream_read(self, n): + data = jarray.zeros(n, 'b') + m = self.istream.read(data) + if m == -1: # indicates EOF has been reached, so we just return the empty string + return "" + elif m <= 0: + return "" + if m < n: + data = data[:m] + return data.tostring() + + def read(self, size=-1): + data = self.buffer + if size < 0: + # Read until EOF + buffers = [] + if data: + buffers.append(data) + self.buffer = "" + recv_size = self.buffer_size + while True: + data = self.istream_read(recv_size) + if not data: + break + buffers.append(data) + return "".join(buffers) + else: + # Read until size bytes or EOF seen, whichever comes first + buf_len = len(data) + if buf_len >= size: + self.buffer = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self.buffer = "" + while True: + left = size - buf_len + recv_size = max(self.buffer_size, left) + data = self.istream_read(recv_size) + if not data: + break + buffers.append(data) + n = len(data) + if n >= left: + self.buffer = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return "".join(buffers) + + def readline(self, size=-1): + data = self.buffer + if size < 0: + # Read until \n or EOF, whichever comes first + nl = data.find('\n') + if nl >= 0: + nl += 1 + self.buffer = data[nl:] + return data[:nl] + buffers = [] + if data: + buffers.append(data) + self.buffer = "" + while True: + data = self.istream_read(self.buffer_size) + if not data: + break + buffers.append(data) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self.buffer = data[nl:] + buffers[-1] = data[:nl] + break + return "".join(buffers) + else: + # Read until size bytes or \n or EOF seen, whichever comes first + nl = data.find('\n', 0, size) + if nl >= 0: + nl += 1 + self.buffer = data[nl:] + return data[:nl] + buf_len = len(data) + if buf_len >= size: + self.buffer = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self.buffer = "" + while True: + data = self.istream_read(self.buffer_size) + if not data: + break + buffers.append(data) + left = size - buf_len + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + self.buffer = data[nl:] + buffers[-1] = data[:nl] + break + n = len(data) + if n >= left: + self.buffer = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return "".join(buffers) + + def readlines(self, sizehint=0): + total = 0 + list = [] + while True: + line = self.readline() + if not line: + break + list.append(line) + total += len(line) + if sizehint and total >= sizehint: + break + return list + + # Iterator protocols + + def __iter__(self): + return self + + def next(self): + line = self.readline() + if not line: + raise StopIteration + return line diff --git a/Lib/modjy/modjy_wsgi.py b/Lib/modjy/modjy_wsgi.py --- a/Lib/modjy/modjy_wsgi.py +++ b/Lib/modjy/modjy_wsgi.py @@ -28,6 +28,7 @@ create_py_file = PyFile from modjy_exceptions import * +from modjy_input import modjy_input_object server_name = "modjy" server_param_prefix = "%s.param" % server_name @@ -125,8 +126,8 @@ def set_wsgi_streams(self, req, resp, dict): try: - dict["wsgi.input"] = create_py_file(req.getInputStream(), "rb") - dict["wsgi.errors"] = create_py_file(System.err) + dict["wsgi.input"] = modjy_input_object(req.getInputStream()) + dict["wsgi.errors"] = create_py_file(System.err) except IOException, iox: raise ModjyIOException(iox) diff --git a/tests/modjy/java/com/xhaus/modjy/ModjyTestPostData.java b/tests/modjy/java/com/xhaus/modjy/ModjyTestPostData.java --- a/tests/modjy/java/com/xhaus/modjy/ModjyTestPostData.java +++ b/tests/modjy/java/com/xhaus/modjy/ModjyTestPostData.java @@ -27,7 +27,7 @@ setAppFile("post_data_tests.py"); } - public void doHeaderTest(String appName, String postData) throws Exception { + public void doPostTest(String appName, String postData) throws Exception { postDataTestSetUp(); setMethod("POST"); setAppName(appName); @@ -40,7 +40,7 @@ public void testPostDataLineEndsNotTranslated() throws Exception { String testData = "this\r\ndata\r\ncontains\r\ncarriage\r\nreturns\r\n"; String expectedData = "'"+testData.replace("\r", "\\r").replace("\n", "\\n")+"'"; - doHeaderTest("test_return_post_data", testData); + doPostTest("test_return_post_data", testData); assertEquals("Wrong post data returned >>" + getOutput() + "<< != >>"+expectedData+"<<", expectedData, getOutput()); } diff --git a/tests/modjy/java/com/xhaus/modjy/ModjyTestWSGIStreams.java b/tests/modjy/java/com/xhaus/modjy/ModjyTestWSGIStreams.java --- a/tests/modjy/java/com/xhaus/modjy/ModjyTestWSGIStreams.java +++ b/tests/modjy/java/com/xhaus/modjy/ModjyTestWSGIStreams.java @@ -63,6 +63,14 @@ doInputTest(appName, bodyContent, expectedContent, expectedLength, 0); } + protected String buildStringWithContents(int count, String contents) { + StringBuilder builder = new StringBuilder(); + for (int i = 0 ; i < count ; i++) { + builder.append(contents); + }; + return builder.toString(); + } + public void testEmptyInput() throws Exception { doInputTest("test_read_input_stream", "", "", 0); } @@ -81,6 +89,14 @@ } } + public void testAsciiInputWithReadSizeLargeInput() throws Exception { + String one_k = buildStringWithContents(1024, "X"); + String testData = buildStringWithContents(64, one_k); + for (int i = 64; i <= 65536; i=i*2) { + doInputTest("test_read_input_stream", testData, testData, testData.length(), i); + } + } + public void testAsciiInputReadline() throws Exception { doInputTest("test_readline_input_stream", "Hello\nWorld!\n", "Hello\n", 6); doInputTest("test_readline_input_stream", "Hello", "Hello", 5); @@ -88,17 +104,40 @@ public void testAsciiInputReadlineWithSize() throws Exception { // Let's test this: although PEP-333 says it's not supported, modjy can do it - doInputTest("test_readline_input_stream", "Hello\nWorld!\n", "Hello", 5, 5); + doInputTest("test_readline_input_stream", "Hello\nWorld!\n", "Hello\n", 6, 6); + } + + public void testAsciiInputReadlineWithSizeLessThanOneLine() throws Exception { + // Test where the size given is too small to actually reach a line end and + // a truncated line should be returned. + doInputTest("test_readline_input_stream", "Hello\nWorld!\n", "Hel", 3, 3); + } + + public void testAsciiInputReadlineWithSizeLongerThanOneLine() throws Exception { + // Test where the size given is long enough to take in a whole line + // and part of the next line. + doInputTest("test_readline_input_stream", "Hello\nWorld!\n", "Hello\n", 6, 10); + } + + public void testAsciiInputReadlineWithSizeLongerThanTwoLines() throws Exception { + // Test where the size given is long enough to take in a whole line + // and part of the next line. + doInputTest("test_readline_input_stream", "Hello\nWorld!\n", "Hello\n", 6, 32); } public void testAsciiInputWithReadlines() throws Exception { + doInputTest("test_readlines_input_stream", "Hello", "Hello", 5); + doInputTest("test_readlines_input_stream", "Hello\n", "Hello\n", 6); doInputTest("test_readlines_input_stream", "Hello\nWorld!\n", "Hello\n$World!\n", 14); - doInputTest("test_readlines_input_stream", "Hello", "Hello", 5); } public void testAsciiInputWithReadlinesWithHint() throws Exception { - // Let's leave this for now - // doInputTest("test_readlines_input_stream", "Hello\nWorld!\n", "Hello\n", 6, 5); + doInputTest("test_readlines_input_stream", "Hello\nWorld!\n", "Hello\n", 6, 5); + doInputTest("test_readlines_input_stream", "Hello\nWorld!\n", "Hello\n$World!\n", 14, 32); + } + + public void testInputIterator() throws Exception { + doInputTest("test_iter_input_stream", "Hello\nWorld!\n", "Hello\n$World!\n", 14); } public void testError() throws Exception { diff --git a/tests/modjy/test_apps_dir/stream_tests.py b/tests/modjy/test_apps_dir/stream_tests.py --- a/tests/modjy/test_apps_dir/stream_tests.py +++ b/tests/modjy/test_apps_dir/stream_tests.py @@ -83,6 +83,14 @@ writer(repr(output_dict)) return [] +def test_iter_input_stream(environ, start_response): + writer = start_response("200 OK", []) + wsgi_input = environ['wsgi.input'] + data = "$".join([line for line in wsgi_input]) + output_dict = {'data': data} + writer(repr(output_dict)) + return [] + def test_error_stream(environ, start_response): writer = start_response("200 OK", []) wsgi_errors = environ['wsgi.errors'] -- Repository URL: http://hg.python.org/jython From jython-checkins at python.org Sun Nov 25 19:42:58 2012 From: jython-checkins at python.org (jeff.allen) Date: Sun, 25 Nov 2012 19:42:58 +0100 (CET) Subject: [Jython-checkins] =?utf-8?q?jython=3A_Buffer_API=3A_beef_up_PyBuf?= =?utf-8?q?ferTest=2Ejava_and_fix_zero-length_slice_bug=2E?= Message-ID: <3Y8g9t5JnczQSj@mail.python.org> http://hg.python.org/jython/rev/6165aa16accf changeset: 6886:6165aa16accf user: Jeff Allen date: Sun Nov 25 17:33:08 2012 +0000 summary: Buffer API: beef up PyBufferTest.java and fix zero-length slice bug. Work on the io library revealed a bug in slicing that affected zero-length slices. I built up the unit test where it dealt with slices. This showed up the zero-length bug, other new bugs, and some weak design that I rectified by removal of BufferPointer.size. PyBufferTest, test_bytes, test_memoryview pass. files: src/org/python/core/BufferPointer.java | 34 +- src/org/python/core/PyBUF.java | 12 +- src/org/python/core/PyBuffer.java | 23 +- src/org/python/core/buffer/BaseBuffer.java | 111 +- src/org/python/core/buffer/SimpleBuffer.java | 36 +- src/org/python/core/buffer/SimpleWritableBuffer.java | 30 +- src/org/python/core/buffer/Strided1DBuffer.java | 72 +- src/org/python/core/buffer/Strided1DWritableBuffer.java | 62 +- src/org/python/core/buffer/ZeroByteBuffer.java | 179 + src/org/python/core/util/StringUtil.java | 8 +- tests/java/org/python/core/PyBufferTest.java | 1284 +++++++-- 11 files changed, 1309 insertions(+), 542 deletions(-) diff --git a/src/org/python/core/BufferPointer.java b/src/org/python/core/BufferPointer.java --- a/src/org/python/core/BufferPointer.java +++ b/src/org/python/core/BufferPointer.java @@ -1,49 +1,41 @@ package org.python.core; /** - * A class that references a contiguous slice of a byte[] array for use in the buffer - * API. This class simply bundles together a refernce to an array, a starting offset within that - * array, and specification of the number of bytes that may validly be accessed at that offset. It - * is used by the Jython buffer API roughly where the CPython buffer API uses a C (char *) pointer, - * or such a pointer and a length. + * A class that references a specified byte[] array and an offset in it to be treated + * as "index zero", for use in the buffer API. This class simply bundles together a reference to an + * array and a particular offset within that array. It is used by the Jython buffer API roughly + * where the CPython buffer API uses a C (char *) pointer. */ public class BufferPointer { /** - * Reference to the array holding the bytes. Usually this is the actual storage exported by a - * Python object. In some contexts the consumer will be entitled to make changes to the contents - * of this array, and in others, not. See {@link PyBuffer#isReadonly()}. + * Reference to the backing array. Usually this is the actual storage exported by a Python + * object. In some contexts the consumer will be entitled to make changes to the contents of + * this array, and in others, not. See {@link PyBuffer#isReadonly()}. */ public final byte[] storage; - /** Starting position within the array for the data being pointed to. */ + /** Starting position within the array for index calculations: "index zero". */ public final int offset; - /** Number of bytes within the array comprising the data being pointed to. */ - public final int size; /** - * Refer to a contiguous slice of the given array. - * + * Refer to an offset in the given array. + * * @param storage array at reference * @param offset index of the first byte - * @param size number of bytes being referred to */ - public BufferPointer(byte[] storage, int offset, int size) { - if ((offset | size | (storage.length-(offset + size))) < 0) { - throw Py.BufferError("Indexing error in buffer API"); - } + public BufferPointer(byte[] storage, int offset) { + // No checks: keep it simple this.storage = storage; this.offset = offset; - this.size = size; } /** * Refer to the whole of a byte array. - * + * * @param storage array at reference */ public BufferPointer(byte[] storage) { this.storage = storage; this.offset = 0; - this.size = storage.length; } } \ No newline at end of file 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 @@ -200,16 +200,16 @@ */ static final int RECORDS_RO = STRIDES | FORMAT; /** - * Equivalent to (INDIRECT | WRITABLE | FORMAT). Also use this as the request flags - * if you plan only to use the fully-encapsulated API (byteAt, storeAt - * , copyTo, copyFrom, etc.), without ever calling + * Equivalent to (INDIRECT | WRITABLE | FORMAT). Also use this in the request if + * you plan only to use the fully-encapsulated API (byteAt, storeAt, + * copyTo, copyFrom, etc.), without ever calling * {@link PyBuffer#getBuf()}. */ static final int FULL = INDIRECT | WRITABLE | FORMAT; /** - * Equivalent to (INDIRECT | FORMAT) Also use this as the request flags if you plan - * only to use the fully-encapsulated API (byteAt, copyTo, etc.), - * without ever calling {@link PyBuffer#getBuf()}. + * Equivalent to (INDIRECT | FORMAT). Also use this in the request if you plan only + * to use the fully-encapsulated API (byteAt, copyTo, etc.), read + * only, without ever calling {@link PyBuffer#getBuf()}. */ static final int FULL_RO = INDIRECT | FORMAT; 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 @@ -260,7 +260,7 @@ BufferPointer getBuf(); /** - * Return a structure describing the slice of a byte array that holds a single item from the + * Return a structure describing the slice of a byte array that points to a single item from the * data being exported to the consumer. For a one-dimensional contiguous buffer, assuming the * following client code where obj has type BufferProtocol: * @@ -275,9 +275,9 @@ * [b.offset] to [b.offset + itemsize - 1] inclusive. And if * itemsize==1, the item is simply the byte b.storage[b.offset] *

- * Essentially this is a method for computing the offset of a particular index. Although - * b.size==itemsize, the client is free to navigate the underlying buffer - * b.storage without respecting these boundaries. + * Essentially this is a method for computing the offset of a particular index. The client is + * free to navigate the underlying buffer b.storage without respecting these + * boundaries. * * @param index in the buffer to position the pointer * @return structure defining the byte[] slice that is the shared data @@ -285,7 +285,7 @@ BufferPointer getPointer(int index); /** - * Return a structure describing the slice of a byte array that holds a single item from the + * Return a structure describing the slice of a byte array that points to a single item from the * data being exported to the consumer, in the case that array may be multi-dimensional. For a * 3-dimensional contiguous buffer, assuming the following client code where obj * has type BufferProtocol: @@ -302,9 +302,9 @@ * [b.offset] to [b.offset + itemsize - 1] inclusive. And if * itemsize==1, the item is simply the byte b.storage[b.offset] *

- * Essentially this is a method for computing the offset of a particular index. Although - * b.size==itemsize, the client is free to navigate the underlying buffer - * b.storage without respecting these boundaries. + * Essentially this is a method for computing the offset of a particular index. The client is + * free to navigate the underlying buffer b.storage without respecting these + * boundaries. *

* If the buffer is also non-contiguous, b.storage[b.offset] is still the (first * byte of) the item at index [0,...,0]. However, it is necessary to navigate b @@ -338,4 +338,11 @@ // Inherited from PyBUF and belonging here // // int getItemsize(); + + /** + * The toString() method of a buffer reproduces the byte values in the buffer (treated as + * unsigned integers) as the character codes of a String. + */ + @Override + public String toString(); } 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,14 +9,14 @@ /** * Base implementation of the Buffer API providing variables and accessors for the navigational - * arrays (without actually creating the arrays), methods for expressing and checking the buffer - * request flags, methods and mechanism for get-release counting, boilerplate error checks and their - * associated exceptions, and default implementations of some methods for access to the buffer - * content. The design aim is to ensure unglamorous common code need only be implemented once. + * arrays, methods for expressing and checking the buffer request flags, methods and mechanism for + * get-release counting, boilerplate error checks and their associated exceptions, and default + * implementations of some methods for access to the buffer content. The design aim is to ensure + * unglamorous common code need only be implemented once. *

* Where provided, the buffer access methods are appropriate to 1-dimensional arrays where the units * are single bytes, stored contiguously. Sub-classes that deal with N-dimensional arrays, - * discontiguous storage and items that are not single bytes must override the default + * non-contiguous storage and items that are not single bytes must override the default * implementations. *

* This base implementation is writable only if {@link PyBUF#WRITABLE} is in the feature flags @@ -200,7 +200,8 @@ @Override public boolean isReadonly() { - return (gFeatureFlags & WRITABLE) == 0; + // WRITABLE is a non-navigational flag, so is inverted in gFeatureFlags + return (gFeatureFlags & WRITABLE) != 0; } @Override @@ -216,7 +217,7 @@ @Override public int getLen() { - // Correct if one-dimensional. Override if N-dimensional with itemsize*product(shape). + // Correct if one-dimensional bytes. Override with itemsize*product(shape). return shape[0]; } @@ -334,14 +335,14 @@ System.arraycopy(buf.storage, s, dest, d, length * itemsize); } else if (itemsize == 1) { - // Discontiguous copy: single byte items + // Non-contiguous copy: single byte items int limit = s + length * stride; for (; s < limit; s += stride) { dest[d++] = buf.storage[s]; } } else { - // Discontiguous copy: each time, copy itemsize bytes then skip + // Non-contiguous copy: each time, copy itemsize bytes then skip int limit = s + length * stride; for (; s < limit; s += skip) { int t = s + itemsize; @@ -383,14 +384,14 @@ System.arraycopy(src, srcPos, buf.storage, d, length * itemsize); } else if (itemsize == 1) { - // Discontiguous copy: single byte items + // Non-contiguous copy: single byte items int limit = d + length * stride; for (; d != limit; d += stride) { buf.storage[d] = src[s++]; } } else { - // Discontiguous copy: each time, copy itemsize bytes then skip + // Non-contiguous copy: each time, copy itemsize bytes then skip int limit = d + length * stride; for (; d != limit; d += skip) { int t = d + itemsize; @@ -414,7 +415,7 @@ // Block operation if read-only and same length if (isReadonly()) { throw notWritable(); - } else if (src.getLen() != buf.size || src.getItemsize() != getItemsize()) { + } else if (src.getLen() != getLen() || src.getItemsize() != getItemsize()) { throw differentStructure(); } @@ -432,14 +433,14 @@ src.copyTo(buf.storage, d); } else if (itemsize == 1) { - // Discontiguous copy: single byte items + // Non-contiguous copy: single byte items int limit = d + src.getLen() * stride; for (; d != limit; d += stride) { buf.storage[d] = src.byteAt(s++); } } else { - // Discontiguous copy: each time, copy itemsize bytes then skip + // Non-contiguous copy: each time, copy itemsize bytes then skip int limit = d + src.getShape()[0] * stride; for (; d != limit; d += stride) { BufferPointer srcItem = src.getPointer(s++); @@ -467,8 +468,8 @@ * buffer view of the exporter's state all remain valid. We do not let consumers do this through * the {@link PyBuffer} interface: from their perspective, calling {@link PyBuffer#release()} * should mean the end of their access, although we can't stop them holding a reference to the - * PyBuffer. Only the exporting object, which is handles the implementation type is trusted to - * know when re-use is safe. + * PyBuffer. Only the exporting object, which handles the implementation type is trusted to know + * when re-use is safe. *

* An exporter will use this method as part of its implementation of * {@link BufferProtocol#getBuffer(int)}. On return from that, the buffer and the exporting @@ -526,12 +527,12 @@ @Override public BufferPointer getPointer(int index) { - return new BufferPointer(buf.storage, calcIndex(index), getItemsize()); + return new BufferPointer(buf.storage, calcIndex(index)); } @Override public BufferPointer getPointer(int... indices) { - return new BufferPointer(buf.storage, calcIndex(indices), getItemsize()); + return new BufferPointer(buf.storage, calcIndex(indices)); } @Override @@ -619,38 +620,54 @@ } /** - * Check that the argument is within the buffer buf. An exception is raised if - * i<buf.offset or i>buf.offset+buf.size-1 + * Convenience method for checking arguments to slice formation, that the start and end elements + * are within the buffer. An exception is raised if start<0 or + * start+length>shape[0]. This logic is correct for one-dimensional arrays (of + * any item size) and stride. In the context we use this, length is guaranteed by + * the acller to be non-negative. * - * @param i index to check - * @throws IndexOutOfBoundsException if i<buf.offset or - * i>buf.offset+buf.size-1. + * @param start index to check + * @param length number of elements in slice (must be >0) + * @throws IndexOutOfBoundsException */ - protected void checkInBuf(int i) throws IndexOutOfBoundsException { - int a = buf.offset; - int b = a + buf.size - 1; - // Check: b >= i >= a. Cheat. - if (((i - a) | (b - i)) < 0) { - throw new IndexOutOfBoundsException(); + protected void checkSlice(int start, int length) throws IndexOutOfBoundsException { + // Between the last element of the slice and the end of the buffer there are ... + int margin = shape[0] - start - length; + if ((start | margin) < 0) { + throw new IndexOutOfBoundsException("invalid slice of buffer"); } } /** - * Check that the both arguments are within the buffer buf. An exception is raised - * if i<buf.offset, j<buf.offset, - * i>buf.offset+buf.size-1, or j>buf.offset+buf.size-1 + * Convenience method for checking arguments to slice formation, that the start and end elements + * are within the buffer. An exception is raised if either of start or + * end=start+(length-1)*stride+1 is <0 or >shape[0] + * . This logic is correct for one-dimensional arrays (of any item size) and current stride. + * Note that the parameter stride is in terms of this biuffer's indexing. In the context we use + * this, length is guaranteed by the acller to be non-negative. * - * @param i index to check - * @param j index to check - * @throws IndexOutOfBoundsException if i<buf.offset or - * i>buf.offset+buf.size-1 + * @param start index to check + * @param length number of elements in slice (must be >0) + * @param stride in buffer elements between items of the slice + * @throws IndexOutOfBoundsException */ - protected void checkInBuf(int i, int j) throws IndexOutOfBoundsException { - int a = buf.offset; - int b = a + buf.size - 1; - // Check: b >= i >= a and b >= j >= a. Cheat. - if (((i - a) | (j - a) | (b - i) | (b - j)) < 0) { - throw new IndexOutOfBoundsException(); + protected void checkSlice(int start, int length, int stride) throws IndexOutOfBoundsException { + /* + * Simpler to check if we know which is the smaller of the two ends, which depends on the + * sign of the stride. + */ + int lo, hi; + if (stride > 0) { + lo = start; + hi = start + (length - 1) * stride + 1; + } else { + hi = start; + lo = start + (length - 1) * stride + 1; + } + // Between the last element of the slice and the end of the buffer there are ... + int margin = shape[0] - hi; + if ((lo | margin) < 0) { + throw new IndexOutOfBoundsException("invalid slice of buffer"); } } @@ -677,11 +694,11 @@ private static PyException bufferErrorFromSyndrome(int syndrome) { if ((syndrome & ND) != 0) { - return bufferRequires("shape"); + return bufferRequires("shape array"); } else if ((syndrome & STRIDES) != 0) { - return bufferRequires("strides"); + return bufferRequires("strides array"); } else if ((syndrome & INDIRECT) != 0) { - return bufferRequires("suboffsets"); + return bufferRequires("suboffsets array"); } else if ((syndrome & WRITABLE) != 0) { return bufferIsNot("writable"); } else if ((syndrome & C_CONTIGUOUS) != 0) { @@ -729,13 +746,13 @@ /** * Convenience method to create (for the caller to throw) a - * BufferError("underlying buffer requires {feature}"). + * BufferError("buffer structure requires consumer to use {feature}"). * * @param feature * @return the error as a PyException */ protected static PyException bufferRequires(String feature) { - return Py.BufferError("underlying buffer requires " + feature); + return Py.BufferError("buffer structure requires consumer to use " + feature); } /** 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 @@ -3,6 +3,7 @@ import org.python.core.BufferPointer; import org.python.core.PyBuffer; import org.python.core.PyException; +import org.python.core.util.StringUtil; /** * Buffer API over a read-only one-dimensional array of one-byte items. @@ -49,7 +50,7 @@ public SimpleBuffer(int flags, byte[] storage, int offset, int size) throws PyException { this(); // Wrap the exported data on a BufferPointer object - this.buf = new BufferPointer(storage, offset, size); + this.buf = new BufferPointer(storage, offset); this.shape[0] = size; // Number of units in exported data checkRequestFlags(flags); // Check request is compatible with type } @@ -134,12 +135,17 @@ @Override public PyBuffer getBufferSlice(int flags, int start, int length) { - // Translate relative to underlying buffer - int compIndex0 = buf.offset + start; - // Check the arguments define a slice within this buffer - checkInBuf(compIndex0, compIndex0 + length - 1); - // Create the slice from the sub-range of the buffer - return new SimpleView(getRoot(), flags, buf.storage, compIndex0, length); + if (length > 0) { + // Check the arguments define a slice within this buffer + checkSlice(start, length); + // Translate relative to underlying buffer + int compIndex0 = buf.offset + start; + // Create the slice from the sub-range of the buffer + return new SimpleView(getRoot(), flags, buf.storage, compIndex0, length); + } else { + // Special case for length==0 where above logic would fail. Efficient too. + return new ZeroByteBuffer.View(getRoot(), flags); + } } /** @@ -154,24 +160,24 @@ @Override public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { - if (stride == 1) { + if (stride == 1 || length < 2) { // Unstrided slice of simple buffer is itself simple return getBufferSlice(flags, start, length); } else { + // Check the arguments define a slice within this buffer + checkSlice(start, length, stride); // Translate relative to underlying buffer int compIndex0 = buf.offset + start; - // Check the slice sits within the present buffer (first and last indexes) - checkInBuf(compIndex0, compIndex0 + (length - 1) * stride); // Construct a view, taking a lock on the root object (this or this.root) return new Strided1DBuffer.SlicedView(getRoot(), flags, buf.storage, compIndex0, - length, stride); + length, stride); } } @Override public BufferPointer getPointer(int index) { - return new BufferPointer(buf.storage, buf.offset + index, 1); + return new BufferPointer(buf.storage, buf.offset + index); } @Override @@ -180,6 +186,12 @@ return getPointer(indices[0]); } + @Override + public String toString() { + // For contiguous bytes in one dimension we can avoid the intAt() calls + return StringUtil.fromBytes(buf.storage, buf.offset, shape[0]); + } + /** * A SimpleBuffer.SimpleView represents a contiguous subsequence of another * SimpleBuffer. 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 @@ -23,7 +23,7 @@ public SimpleWritableBuffer(int flags, byte[] storage, int offset, int size) throws PyException { addFeatureFlags(WRITABLE); // Wrap the exported data on a BufferPointer object - this.buf = new BufferPointer(storage, offset, size); + this.buf = new BufferPointer(storage, offset); this.shape[0] = size; // Number of units in exported data checkRequestFlags(flags); // Check request is compatible with type } @@ -90,7 +90,7 @@ @Override public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { - if (src.getLen() != buf.size) { + if (src.getLen() != getLen()) { throw differentStructure(); } @@ -106,12 +106,17 @@ */ @Override public PyBuffer getBufferSlice(int flags, int start, int length) { - // Translate relative to underlying buffer - int compIndex0 = buf.offset + start; - // Check the arguments define a slice within this buffer - checkInBuf(compIndex0, compIndex0 + length - 1); - // Create the slice from the sub-range of the buffer - return new SimpleView(getRoot(), flags, buf.storage, compIndex0, length); + if (length > 0) { + // Check the arguments define a slice within this buffer + checkSlice(start, length); + // Translate relative to underlying buffer + int compIndex0 = buf.offset + start; + // Create the slice from the sub-range of the buffer + return new SimpleView(getRoot(), flags, buf.storage, compIndex0, length); + } else { + // Special case for length==0 where above logic would fail. Efficient too. + return new ZeroByteBuffer.View(getRoot(), flags); + } } /** @@ -120,20 +125,21 @@ * SimpleWritableBuffer provides an implementation ensuring the returned slice is * writable. */ + @Override public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { - if (stride == 1) { + if (stride == 1 || length < 2) { // Unstrided slice of simple buffer is itself simple return getBufferSlice(flags, start, length); } else { + // Check the arguments define a slice within this buffer + checkSlice(start, length, stride); // Translate relative to underlying buffer int compIndex0 = buf.offset + start; - // Check the slice sits within the present buffer (first and last indexes) - checkInBuf(compIndex0, compIndex0 + (length - 1) * stride); // Construct a view, taking a lock on the root object (this or this.root) return new Strided1DWritableBuffer.SlicedView(getRoot(), flags, buf.storage, - compIndex0, length, stride); + compIndex0, length, stride); } } 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 @@ -36,15 +36,6 @@ protected int stride; /** - * Absolute index in buf.storage of item[0]. For a positive - * stride this is equal to buf.offset, and for a negative - * stride it is buf.offset+buf.size-1. It has to be used in most of - * the places that buf.offset would appear in the index calculations of simpler buffers (that - * have unit stride). - */ - protected int index0; - - /** * Provide an instance of Strided1DBuffer with navigation variables partly * initialised, for sub-class use. To complete initialisation, the sub-class normally must * assign: {@link #buf}, {@link #shape}[0], and {@link #stride}, and call @@ -90,28 +81,15 @@ */ public Strided1DBuffer(int flags, byte[] storage, int index0, int length, int stride) throws PyException { - // Arguments programme the object directly this(); this.shape[0] = length; - this.index0 = index0; + this.buf = new BufferPointer(storage, index0); this.stride = stride; - - // Calculate buffer offset and size: start with distance of last item from first - int d = (length - 1) * stride; - - if (stride >= 0) { - // Positive stride: indexing runs from first item - this.buf = new BufferPointer(storage, index0, 1 + d); - if (stride <= 1) { - // Really this is a simple buffer - addFeatureFlags(CONTIGUITY); - } - } else { - // Negative stride: indexing runs from last item - this.buf = new BufferPointer(storage, index0 + d, 1 - d); + if (stride == 1) { + // Really this is a simple buffer + addFeatureFlags(CONTIGUITY); } - checkRequestFlags(flags); // Check request is compatible with type } @@ -122,12 +100,12 @@ @Override public byte byteAt(int index) throws IndexOutOfBoundsException { - return buf.storage[index0 + index * stride]; + return buf.storage[buf.offset + index * stride]; } @Override protected int calcIndex(int index) throws IndexOutOfBoundsException { - return index0 + index * stride; + return buf.offset + index * stride; } @Override @@ -145,7 +123,7 @@ public void copyTo(int srcIndex, byte[] dest, int destPos, int length) throws IndexOutOfBoundsException { // Data is here in the buffers - int s = index0 + srcIndex * stride; + int s = buf.offset + srcIndex * stride; int d = destPos; // Strategy depends on whether items are laid end-to-end contiguously or there are gaps @@ -154,7 +132,7 @@ System.arraycopy(buf.storage, s, dest, d, length); } else { - // Discontiguous copy: single byte items + // Non-contiguous copy: single byte items int limit = s + length * stride; for (; s != limit; s += stride) { dest[d++] = buf.storage[s]; @@ -171,22 +149,38 @@ * which x was created from u. Thus y(k) = u(r+sp+kmp), that is, the * composite offset is r+sp and the composite stride is mp. */ + @Override public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { - // Translate relative to underlying buffer - int compStride = this.stride * stride; - int compIndex0 = index0 + start * stride; + if (length > 0) { + int compStride; - // Check the slice sits within the present buffer (first and last indexes) - checkInBuf(compIndex0, compIndex0 + (length - 1) * compStride); + if (stride == 1) { + // Check the arguments define a slice within this buffer + checkSlice(start, length); + // Composite stride is same as original stride + compStride = this.stride; + } else { + // Check the arguments define a slice within this buffer + checkSlice(start, length, stride); + // Composite stride is product + compStride = this.stride * stride; + } - // Construct a view, taking a lock on the root object (this or this.root) - return new SlicedView(getRoot(), flags, buf.storage, compIndex0, length, compStride); + // Translate start relative to underlying buffer + int compIndex0 = buf.offset + start * this.stride; + // Construct a view, taking a lock on the root object (this or this.root) + return new SlicedView(getRoot(), flags, buf.storage, compIndex0, length, compStride); + + } else { + // Special case for length==0 where above logic would fail. Efficient too. + return new ZeroByteBuffer.View(getRoot(), flags); + } } @Override public BufferPointer getPointer(int index) { - return new BufferPointer(buf.storage, index0 + index, 1); + return new BufferPointer(buf.storage, buf.offset + index * stride); } @Override @@ -206,7 +200,7 @@ } /** - * A Strided1DBuffer.SlicedView represents a discontiguous subsequence of a simple + * A Strided1DBuffer.SlicedView represents a non-contiguous subsequence of a simple * buffer. */ static class SlicedView extends Strided1DBuffer { 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 @@ -34,28 +34,16 @@ */ public Strided1DWritableBuffer(int flags, byte[] storage, int index0, int length, int stride) throws PyException { - // Arguments programme the object directly // this(); this.shape[0] = length; - this.index0 = index0; + this.buf = new BufferPointer(storage, index0); this.stride = stride; - - // Calculate buffer offset and size: start with distance of last item from first - int d = (length - 1) * stride; - - if (stride >= 0) { - // Positive stride: indexing runs from first item - this.buf = new BufferPointer(storage, index0, 1 + d); - if (stride <= 1) { - // Really this is a simple buffer - addFeatureFlags(CONTIGUITY); - } - } else { - // Negative stride: indexing runs from last item - this.buf = new BufferPointer(storage, index0 + d, 1 - d); + if (stride == 1) { + // Really this is a simple buffer + addFeatureFlags(CONTIGUITY); } - + addFeatureFlags(WRITABLE); checkRequestFlags(flags); // Check request is compatible with type } @@ -66,7 +54,7 @@ @Override public void storeAt(byte value, int index) throws IndexOutOfBoundsException, PyException { - buf.storage[index0 + index * stride] = value; + buf.storage[buf.offset + index * stride] = value; } /** @@ -79,7 +67,7 @@ // Data is here in the buffers int s = srcPos; - int d = index0 + destIndex * stride; + int d = buf.offset + destIndex * stride; // Strategy depends on whether items are laid end-to-end or there are gaps if (stride == 1) { @@ -87,7 +75,7 @@ System.arraycopy(src, srcPos, buf.storage, d, length); } else { - // Discontiguous copy: single byte items + // Non-contiguous copy: single byte items int limit = d + length * stride; for (; d != limit; d += stride) { buf.storage[d] = src[s++]; @@ -101,22 +89,38 @@ * Strided1DWritableBuffer provides an implementation that returns a writable * slice. */ + @Override public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { - // Translate relative to underlying buffer - int compStride = this.stride * stride; - int compIndex0 = index0 + start * stride; + if (length > 0) { + int compStride; - // Check the slice sits within the present buffer (first and last indexes) - checkInBuf(compIndex0, compIndex0 + (length - 1) * compStride); + if (stride == 1) { + // Check the arguments define a slice within this buffer + checkSlice(start, length); + // Composite stride is same as original stride + compStride = this.stride; + } else { + // Check the arguments define a slice within this buffer + checkSlice(start, length, stride); + // Composite stride is product + compStride = this.stride * stride; + } - // Construct a view, taking a lock on the root object (this or this.root) - return new SlicedView(getRoot(), flags, buf.storage, compIndex0, length, compStride); + // Translate start relative to underlying buffer + int compIndex0 = buf.offset + start * this.stride; + // Construct a view, taking a lock on the root object (this or this.root) + return new SlicedView(getRoot(), flags, buf.storage, compIndex0, length, compStride); + + } else { + // Special case for length==0 where above logic would fail. Efficient too. + return new ZeroByteBuffer.View(getRoot(), flags); + } } /** - * A Strided1DWritableBuffer.SlicedView represents a discontiguous subsequence of a - * simple buffer. + * A Strided1DWritableBuffer.SlicedView represents a non-contiguous subsequence of + * a simple buffer. */ static class SlicedView extends Strided1DWritableBuffer { diff --git a/src/org/python/core/buffer/ZeroByteBuffer.java b/src/org/python/core/buffer/ZeroByteBuffer.java new file mode 100644 --- /dev/null +++ b/src/org/python/core/buffer/ZeroByteBuffer.java @@ -0,0 +1,179 @@ +package org.python.core.buffer; + +import org.python.core.BufferPointer; +import org.python.core.PyBuffer; +import org.python.core.PyException; + +/** + * Buffer API over a zero length, one-dimensional array of one-byte items. The buffer is nominally + * writable, but since there is nowhere to write to, any attempt to write or read throws an + * IndexOutOfBoundsException. This class exists mostly to represent zero-length arrays, + * and particularly, zero-length slices for which implementations of + * {@link PyBuffer#getBufferSlice(int, int, int, int)} in any case need special logic. Bulk + * operations like {@link #copyTo(byte[], int)}) and {@link #toString()} efficiently do nothing, + * instead of calling complicated logic that finally does nothing. + */ +public class ZeroByteBuffer extends BaseBuffer { + + /** Shared instance of a zero-length buffer. */ + private static final BufferPointer EMPTY_BUF = new BufferPointer(new byte[0]); + + /** Array containing a single zero for the length */ + protected static final int[] SHAPE = {0}; + + /** + * Construct an instance of a zero-length buffer, choosing whether it should report itself to be + * read-only through {@link #isReadonly()}. This is moot, as any attempt to write to it produces + * an {@link IndexOutOfBoundsException}, but it is less surprising for client code that may ask, + * if the readability follows that of the object from which the buffer is derived. + * + * @param flags consumer requirements + * @param readonly set true if readonly + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public ZeroByteBuffer(int flags, boolean readonly) throws PyException { + super(CONTIGUITY | SIMPLE | (readonly ? 0 : WRITABLE)); + this.buf = EMPTY_BUF; // Wraps empty array + this.shape = SHAPE; // {0} + this.strides = SimpleBuffer.SIMPLE_STRIDES; // {1} + checkRequestFlags(flags); + } + + @Override + public int getLen() { + return 0; + } + + /** + * In a ZeroByteBuffer, the index is always out of bounds. + */ + @Override + protected int calcIndex(int index) throws IndexOutOfBoundsException { + // This causes all access to the bytes in to throw (since BaseBuffer calls it). + throw new IndexOutOfBoundsException(); + } + + /** + * In a ZeroByteBuffer, if the dimensions are right, the index is out of bounds anyway. + */ + @Override + protected int calcIndex(int... indices) throws IndexOutOfBoundsException { + // Bootless dimension check takes precedence (for consistency with other buffers) + checkDimension(indices); + // This causes all access to the bytes in to throw (since BaseBuffer calls it). + throw new IndexOutOfBoundsException(); + } + + /** + * {@inheritDoc} + *

+ * In a ZeroByteBuffer, there is simply nothing to copy. + */ + @Override + public void copyTo(byte[] dest, int destPos) throws IndexOutOfBoundsException { + // Nothing to copy + } + + /** + * {@inheritDoc} + *

+ * In a ZeroByteBuffer, there is simply nothing to copy. + */ + @Override + public void copyTo(int srcIndex, byte[] dest, int destPos, int length) + throws IndexOutOfBoundsException, PyException { + // Nothing to copy + } + + /** + * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source length + * is zero. + */ + @Override + public void copyFrom(byte[] src, int srcPos, int destIndex, int length) + throws IndexOutOfBoundsException, PyException { + if (length > 0) { + throw new IndexOutOfBoundsException(); + } + } + + /** + * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source length + * is zero. + */ + @Override + public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { + if (src.getLen() > 0) { + throw new IndexOutOfBoundsException(); + } + } + + /** + * Only a zero-length slice at zero is valid (in which case, the present buffer will do nicely + * as a result, with the export count incremented. + */ + @Override + public PyBuffer getBufferSlice(int flags, int start, int length) { + if (start == 0 && length <= 0) { + return this.getBuffer(flags); + } else { + throw new IndexOutOfBoundsException(); + } + } + + /** + * Only a zero-length slice at zero is valid (in which case, the present buffer will do nicely + * as a result, with the export count incremented. + */ + @Override + public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + // It can't matter what the stride is since length is zero, or there's an error. + return getBufferSlice(flags, start, length); + } + + /** + * For a ZeroByteBuffer, it's the empty string. + */ + @Override + public String toString() { + return ""; + } + + /** + * A ZeroByteBuffer.View represents a contiguous subsequence of another + * PyBuffer. We don't need it to make slices of the ZeroByteBuffer itself, but it + * is useful for making zero-length slices of anything else. Lock-release semantics must still + * be observed. In Python, a zero-length slice obtained from the memoryview of a bytearray still + * counts as an export from the bytearray. + */ + static class View extends ZeroByteBuffer { + + /** The buffer on which this is a slice view */ + PyBuffer root; + + /** + * Construct a slice of a ZeroByteBuffer, which it goes without saying is of zero length at + * position zero. + * + * @param root buffer which will be acquired and must be released ultimately + * @param flags the request flags of the consumer that requested the slice + */ + public View(PyBuffer root, int flags) { + // Create a new ZeroByteBuffer on who-cares-what byte array + super(flags, root.isReadonly()); + // But we still have to get a lease on the root PyBuffer + this.root = root.getBuffer(FULL_RO); + } + + @Override + protected PyBuffer getRoot() { + return root; + } + + @Override + public void releaseAction() { + // We have to release the root too if ours was final. + root.release(); + } + } +} diff --git a/src/org/python/core/util/StringUtil.java b/src/org/python/core/util/StringUtil.java --- a/src/org/python/core/util/StringUtil.java +++ b/src/org/python/core/util/StringUtil.java @@ -70,14 +70,14 @@ /** * Return a new String with chars corresponding to buf, which is a byte-oriented buffer obtained - * through the buffer API. - * + * through the buffer API. It depends on the implementation of {@link PyBuffer#toString()} + * provided by each buffer implementation. + * * @param buf a PyBuffer of bytes * @return a new String corresponding to the bytes in buf */ public static String fromBytes(PyBuffer buf) { - BufferPointer bp = buf.getBuf(); - return fromBytes(bp.storage, bp.offset, bp.size); + return buf.toString(); } /** 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 @@ -4,11 +4,14 @@ import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Set; import junit.framework.TestCase; +import org.python.core.buffer.BaseBuffer; import org.python.core.buffer.SimpleBuffer; import org.python.core.buffer.SimpleStringBuffer; import org.python.core.buffer.SimpleWritableBuffer; @@ -30,14 +33,17 @@ * exception. *

* The Jython buffer API follows the structures of the CPython buffer API so that it supports in - * principle the use of multi-dimensional, strided add indirect array structures as buffers. + * principle the use of multi-dimensional, strided and indirect array structures as buffers. * However, actual buffers in the Jython core, and therefore these tests, limit themselves to one - * dimensional contiguous buffers with a simple organisation. Some tests apply directly to the + * dimensional (possibly non-contiguous) directly-indexed buffers. Some tests apply directly to the * N-dimensional cases, and some need a complete re-think. Sub-classing this test would probably be * a good way to extend it to a wider range. */ public class PyBufferTest extends TestCase { + /** Control amount of output. Instance variable so can be adjusted temporarily per test. */ + protected int verbosity = 0; + /** * Generated constructor * @@ -53,51 +59,170 @@ /* * Values for initialising the exporters. */ - private static final ByteMaterial byteMaterial = new ByteMaterial(0, 17, 16); - private static final ByteMaterial abcMaterial = new ByteMaterial("abcdef"); + private static final ByteMaterial byteMaterial = new ByteMaterial(0, 16, 17); + private static final ByteMaterial abcMaterial = new ByteMaterial("abcdefgh"); private static final ByteMaterial stringMaterial = new ByteMaterial("Mon c?t? f?cheux"); private static final ByteMaterial emptyMaterial = new ByteMaterial(new byte[0]); - private static final ByteMaterial longMaterial = new ByteMaterial(0, 5, 1000); + public static final int LONG = 1000; + private static final ByteMaterial longMaterial = new ByteMaterial(0, LONG, 5); + @Override protected void setUp() throws Exception { super.setUp(); // Exception raising requires the Jython interpreter interp = new PythonInterpreter(); - // Tests using local examples - queueWrite(new SimpleWritableExporter(abcMaterial.getBytes()), abcMaterial); - queueReadonly(new SimpleExporter(byteMaterial.getBytes()), byteMaterial); - queueReadonly(new StringExporter(stringMaterial.string), stringMaterial); - queueWrite(new SimpleWritableExporter(emptyMaterial.getBytes()), emptyMaterial); + // Tests using local types of exporter + genWritable(new SimpleWritableExporter(abcMaterial.getBytes()), abcMaterial); + genReadonly(new SimpleExporter(byteMaterial.getBytes()), byteMaterial); + genReadonly(new StringExporter(stringMaterial.string), stringMaterial); + genWritable(new SimpleWritableExporter(emptyMaterial.getBytes()), emptyMaterial); // Tests with PyByteArray - queueWrite(new PyByteArray(abcMaterial.getBytes()), abcMaterial); - queueWrite(new PyByteArray(longMaterial.getBytes()), longMaterial); - queueWrite(new PyByteArray(), emptyMaterial); + genWritable(new PyByteArray(abcMaterial.getBytes()), abcMaterial); + genWritable(new PyByteArray(longMaterial.getBytes()), longMaterial); + genWritable(new PyByteArray(), emptyMaterial); // Tests with PyString - queueReadonly(new PyString(abcMaterial.string), abcMaterial); - queueReadonly(new PyString(), emptyMaterial); + genReadonly(new PyString(abcMaterial.string), abcMaterial); + genReadonly(new PyString(), emptyMaterial); // Ensure case is tested where PyByteArray has an internal offset PyByteArray truncated = new PyByteArray(stringMaterial.getBytes()); truncated.delRange(0, 4); ByteMaterial truncatedMaterial = new ByteMaterial(stringMaterial.string.substring(4)); assert truncated.__alloc__() > truncatedMaterial.length; - queueWrite(truncated, truncatedMaterial); + genWritable(truncated, truncatedMaterial); } - private void queueWrite(BufferProtocol exporter, ByteMaterial material) { - BufferTestPair pair = new BufferTestPair(exporter, material); - buffersToRead.add(pair); - buffersToWrite.add(pair); + /** Generate a series of test material for a writable object. */ + private void genWritable(BufferProtocol exporter, ByteMaterial material) { + generate(exporter, material, false); } - private void queueReadonly(BufferProtocol exporter, ByteMaterial material) { - BufferTestPair pair = new BufferTestPair(exporter, material); + /** Generate a series of test material for a read-only object. */ + private void genReadonly(BufferProtocol exporter, ByteMaterial material) { + generate(exporter, material, true); + } + + /** Lengths we will use if we can when slicing view */ + private static final int[] sliceLengths = {1, 2, 5, 0, LONG / 4}; + + /** Step sizes we will use if we can when slicing view */ + private static final int[] sliceSteps = {1, 2, 3, 7}; + + /** + * Generate a series of test material for a read-only or writable object. Given one exporter, + * and its reference ByteMaterial this method first queues a BufferTestPair corresponding to the + * exporter as the test subject and its test material. This provides a "direct" PyBuffer view on + * the exporter. It then goes on to make a variety of sliced PyBuffer views of the exporter by + * calling {@link PyBuffer#getBufferSlice(int, int, int, int)} on the direct view. The slices + * are made with a variety of argument combinations, filtered down to those that make sense for + * the size of the direct view. Each sliced buffer (considered a test subject now), together + * with correspondingly sliced reference ByteMaterial is queued as BufferTestPair. + * + * @param exporter underlying object + * @param material reference material corresponding to the exporter + * @param readonly whether the exporter is of read-only type + */ + private void generate(BufferProtocol exporter, ByteMaterial material, boolean readonly) { + + // Generate a test using the buffer directly exported by the exporter + PyBuffer direct = queue(exporter, material, readonly); + + // Generate some slices from the material and this direct view + int N = material.length; + int M = (N + 4) / 4; // At least one and about N/4 + + // For a range of start positions up to one beyond the end + for (int start = 0; start <= N; start += M) { + // For a range of lengths + for (int length : sliceLengths) { + + if (length == 0) { + queue(direct, material, start, 0, 1, readonly); + queue(direct, material, start, 0, 2, readonly); + + } else if (length == 1 && start < N) { + queue(direct, material, start, 1, 1, readonly); + queue(direct, material, start, 1, 2, readonly); + + } else if (start < N) { + + // And for a range of step sizes + for (int step : sliceSteps) { + // Check this is a feasible slice + if (start + (length - 1) * step < N) { + queue(direct, material, start, length, step, readonly); + } + } + + // Now use all the step sizes negatively + for (int step : sliceSteps) { + // Check this is a feasible slice + if (start - (length - 1) * step >= 0) { + queue(direct, material, start, length, -step, readonly); + } + } + } + } + } + } + + /** Generate and queue one test of non-slice type (if getting a buffer succeeds). */ + private PyBuffer queue(BufferProtocol exporter, ByteMaterial material, boolean readonly) { + if (verbosity > 2) { + System.out.printf("queue non-slice: length=%d, readonly=%s\n", material.length, + readonly); + } + BufferTestPair pair = new BufferTestPair(exporter, material, readonly); + queue(pair); + return pair.view; + } + + /** Generate and queue one test of slice type (if getting a buffer succeeds). */ + private PyBuffer queue(PyBuffer direct, ByteMaterial material, int start, int length, int step, + boolean readonly) { + + int flags = readonly ? PyBUF.FULL_RO : PyBUF.FULL; + PyBuffer subject = null; + + /* + * Make a slice. We ignore this case if we fail, because we are not testing slice creation + * here, but making slices to be tested as buffers. We'll test slice creation in + * testGetBufferSlice. + */ + try { + if (verbosity > 2) { + System.out.printf(" queue slice: start=%4d, length=%4d, step=%4d\n", start, + length, step); + } + subject = direct.getBufferSlice(flags, start, length, step); + ByteMaterial sliceMaterial = material.slice(start, length, step); + BufferTestPair pair = new BufferTestPair(subject, sliceMaterial, step, readonly); + queue(pair); + } catch (Exception e) { + /* + * We ignore this case if we fail, because we are not testing slice creation here, but + * making slices to be tested as buffers. We'll test slice creation elsewhere. + */ + if (verbosity > 2) { + System.out.printf("*** SKIP %s\n", e); + } + } + + return subject; + } + + /** Queue one instance of test material for a read-only or writable object. */ + private void queue(BufferTestPair pair) { buffersToRead.add(pair); - buffersToFailToWrite.add(pair); + if (pair.readonly) { + buffersToFailToWrite.add(pair); + } else { + buffersToWrite.add(pair); + } } /** Read operations should succeed on all these objects. */ @@ -107,55 +232,24 @@ /** Write operations should fail on all these objects. */ private List buffersToFailToWrite = new LinkedList(); - /** We should be able to get a buffer for all these flag types. */ - private int[] validFlags = {PyBUF.SIMPLE, PyBUF.ND, PyBUF.STRIDES, PyBUF.INDIRECT}; + /** + * A one-dimensional exporter should be able to give us a buffer for all these flag types. + */ + private static final int[] simpleFlags = {PyBUF.SIMPLE, PyBUF.ND, PyBUF.STRIDES, + PyBUF.INDIRECT, PyBUF.FULL_RO}; - /** To which we can add any of these (in one dimension, anyway) */ - private int[] validTassles = {0, - PyBUF.FORMAT, - PyBUF.C_CONTIGUOUS, - PyBUF.F_CONTIGUOUS, - PyBUF.ANY_CONTIGUOUS}; + /** To {@link #simpleFlags} we can add any of these */ + private static final int[] simpleTassles = {0, PyBUF.FORMAT, PyBUF.C_CONTIGUOUS, + PyBUF.F_CONTIGUOUS, PyBUF.ANY_CONTIGUOUS}; /** - * Test method for {@link org.python.core.BufferProtocol#getBuffer()}. + * A one-dimensional exporter with stride!=1 is restricted to give us a buffer only for these + * flag types. */ - public void testExporterGetBuffer() { + private static final int[] strided1DFlags = {PyBUF.STRIDES, PyBUF.INDIRECT, PyBUF.FULL_RO}; - for (BufferTestPair test : buffersToRead) { - System.out.println("getBuffer(): " + test); - for (int flags : validFlags) { - for (int tassle : validTassles) { - PyBuffer view = test.exporter.getBuffer(flags | tassle); - assertNotNull(view); - } - } - } - - for (BufferTestPair test : buffersToWrite) { - System.out.println("getBuffer(WRITABLE): " + test); - for (int flags : validFlags) { - for (int tassle : validTassles) { - PyBuffer view = test.exporter.getBuffer(flags | tassle | PyBUF.WRITABLE); - assertNotNull(view); - } - } - } - - for (BufferTestPair test : buffersToFailToWrite) { - System.out.println("getBuffer(WRITABLE): " + test); - for (int flags : validFlags) { - try { - test.exporter.getBuffer(flags | PyBUF.WRITABLE); - fail("Write access not prevented: " + test); - } catch (PyException pye) { - // Expect BufferError - assertEquals(Py.BufferError, pye.type); - } - } - } - - } + /** To {@link #strided1DFlags} we can add any of these */ + private static final int[] strided1DTassles = {0, PyBUF.FORMAT}; /** * Test method for {@link org.python.core.PyBUF#isReadonly()}. @@ -163,13 +257,17 @@ public void testIsReadonly() { for (BufferTestPair test : buffersToWrite) { - System.out.println("isReadonly: " + test); - assertFalse(test.simple.isReadonly()); + if (verbosity > 0) { + System.out.println("isReadonly: " + test); + } + assertFalse(test.view.isReadonly()); } for (BufferTestPair test : buffersToFailToWrite) { - System.out.println("isReadonly: " + test); - assertTrue(test.simple.isReadonly()); + if (verbosity > 0) { + System.out.println("isReadonly: " + test); + } + assertTrue(test.view.isReadonly()); } } @@ -178,8 +276,10 @@ */ public void testGetNdim() { for (BufferTestPair test : buffersToRead) { - System.out.println("getNdim: " + test); - assertEquals("simple ndim", test.shape.length, test.simple.getNdim()); + if (verbosity > 0) { + System.out.println("getNdim: " + test); + } + assertEquals("unexpected ndim", test.shape.length, test.view.getNdim()); } } @@ -188,10 +288,12 @@ */ public void testGetShape() { for (BufferTestPair test : buffersToRead) { - System.out.println("getShape: " + test); - int[] shape = test.simple.getShape(); - assertNotNull(shape); - assertIntsEqual("simple shape", test.shape, shape); + if (verbosity > 0) { + System.out.println("getShape: " + test); + } + int[] shape = test.view.getShape(); + assertNotNull("shape[] should always be provided", shape); + assertIntsEqual("unexpected shape", test.shape, shape); } } @@ -200,9 +302,10 @@ */ public void testGetLen() { for (BufferTestPair test : buffersToRead) { - System.out.println("getLen: " + test); - assertEquals(" simple len", test.material.bytes.length, test.simple.getLen()); - assertEquals("strided len", test.material.bytes.length, test.strided.getLen()); + if (verbosity > 0) { + System.out.println("getLen: " + test); + } + assertEquals("unexpected length", test.material.length, test.view.getLen()); } } @@ -211,11 +314,13 @@ */ public void testByteAt() { for (BufferTestPair test : buffersToRead) { - System.out.println("byteAt: " + test); + if (verbosity > 0) { + System.out.println("byteAt: " + test); + } int n = test.material.length; byte[] exp = test.material.bytes; for (int i = 0; i < n; i++) { - assertEquals(exp[i], test.simple.byteAt(i)); + assertEquals(exp[i], test.view.byteAt(i)); } } } @@ -226,20 +331,23 @@ public void testByteAtNdim() { int[] index = new int[1]; for (BufferTestPair test : buffersToRead) { - System.out.println("byteAt(array): " + test); - if (test.strided.getShape().length != 1) { - fail("Test not implemented dimensions != 1"); + if (verbosity > 0) { + System.out.println("byteAt(array): " + test); + } + if (test.view.getShape().length != 1) { + fail("Test not implemented if dimensions != 1"); } byte[] exp = test.material.bytes; int n = test.material.length; - // Run through 1D index for simple + // Run through 1D index for view for (int i = 0; i < n; i++) { index[0] = i; - assertEquals(exp[i], test.simple.byteAt(index)); + assertEquals(exp[i], test.view.byteAt(index)); } + // Check 2D index throws try { - test.simple.byteAt(0, 0); + test.view.byteAt(0, 0); fail("Use of 2D index did not raise exception"); } catch (PyException pye) { // Expect BufferError @@ -253,11 +361,13 @@ */ public void testIntAt() { for (BufferTestPair test : buffersToRead) { - System.out.println("intAt: " + test); + if (verbosity > 0) { + System.out.println("intAt: " + test); + } int n = test.material.length; int[] exp = test.material.ints; for (int i = 0; i < n; i++) { - assertEquals(exp[i], test.simple.intAt(i)); + assertEquals(exp[i], test.view.intAt(i)); } } } @@ -268,20 +378,22 @@ public void testIntAtNdim() { int[] index = new int[1]; for (BufferTestPair test : buffersToRead) { - System.out.println("intAt(array): " + test); - if (test.strided.getShape().length != 1) { - fail("Test not implemented dimensions != 1"); + if (verbosity > 0) { + System.out.println("intAt(array): " + test); + } + if (test.view.getShape().length != 1) { + fail("Test not implemented for dimensions != 1"); } int[] exp = test.material.ints; int n = test.material.length; - // Run through 1D index for simple + // Run through 1D index for view for (int i = 0; i < n; i++) { index[0] = i; - assertEquals(exp[i], test.simple.intAt(index)); + assertEquals(exp[i], test.view.intAt(index)); } // Check 2D index throws try { - test.simple.intAt(0, 0); + test.view.intAt(0, 0); fail("Use of 2D index did not raise exception"); } catch (PyException pye) { // Expect BufferError @@ -295,17 +407,19 @@ */ public void testStoreAt() { for (BufferTestPair test : buffersToWrite) { - System.out.println("storeAt: " + test); + if (verbosity > 0) { + System.out.println("storeAt: " + test); + } int n = test.material.length; int[] exp = test.material.ints; // Write modified test material into each location using storeAt() for (int i = 0; i < n; i++) { byte v = (byte)(exp[i] ^ 3); // twiddle some bits - test.simple.storeAt(v, i); + test.view.storeAt(v, i); } // Compare each location with modified test data using intAt() for (int i = 0; i < n; i++) { - assertEquals(exp[i] ^ 3, test.simple.intAt(i)); + assertEquals(exp[i] ^ 3, test.view.intAt(i)); } } } @@ -315,21 +429,23 @@ */ public void testStoreAtNdim() { for (BufferTestPair test : buffersToWrite) { - System.out.println("storeAt: " + test); + if (verbosity > 0) { + System.out.println("storeAt: " + test); + } int n = test.material.length; int[] exp = test.material.ints; // Write modified test material into each location using storeAt() for (int i = 0; i < n; i++) { byte v = (byte)(exp[i] ^ 3); // twiddle some bits - test.simple.storeAt(v, i); + test.view.storeAt(v, i); } // Compare each location with modified test data using intAt() for (int i = 0; i < n; i++) { - assertEquals(exp[i] ^ 3, test.simple.intAt(i)); + assertEquals(exp[i] ^ 3, test.view.intAt(i)); } // Check 2D index throws try { - test.simple.storeAt((byte)1, 0, 0); + test.view.storeAt((byte)1, 0, 0); fail("Use of 2D index did not raise exception"); } catch (PyException pye) { // Expect BufferError @@ -344,15 +460,17 @@ public void testCopyTo() { final int OFFSET = 5; for (BufferTestPair test : buffersToRead) { - System.out.println("copyTo: " + test); + if (verbosity > 0) { + System.out.println("copyTo: " + test); + } int n = test.material.length; // Try with zero offset byte[] actual = new byte[n]; - test.simple.copyTo(actual, 0); + test.view.copyTo(actual, 0); assertBytesEqual("copyTo() incorrect", test.material.bytes, actual, 0); // Try to middle of array actual = new byte[n + 2 * OFFSET]; - test.simple.copyTo(actual, OFFSET); + test.view.copyTo(actual, OFFSET); assertBytesEqual("copyTo(offset) incorrect", test.material.bytes, actual, OFFSET); assertEquals("data before destination", 0, actual[OFFSET - 1]); assertEquals("data after destination", 0, actual[OFFSET + n]); @@ -367,8 +485,10 @@ final byte BLANK = 7; for (BufferTestPair test : buffersToRead) { - System.out.println("copyTo(from slice): " + test); - PyBuffer view = test.simple; + if (verbosity > 0) { + System.out.println("copyTo(from slice): " + test); + } + PyBuffer view = test.view; int n = test.material.length; byte[] actual = new byte[n + 2 * OFFSET]; @@ -380,11 +500,13 @@ // A variety of lengths from zero to (n-srcIndex)-ish for (int length = 0; srcIndex + length <= n; length = 2 * length + 1) { - /* - * System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", - * srcIndex, srcIndex + length, n, destPos, destPos + length, - * actual.length); - */ + + if (verbosity > 1) { + System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", + srcIndex, srcIndex + length, n, destPos, destPos + length, + actual.length); + } + Arrays.fill(actual, BLANK); // Test the method @@ -392,7 +514,7 @@ // Check changed part of destination assertBytesEqual("copyTo(slice) incorrect", test.material.bytes, srcIndex, - actual, destPos, length); + actual, destPos, length); if (destPos > 0) { assertEquals("data before destination", BLANK, actual[destPos - 1]); } @@ -402,11 +524,13 @@ // And from exactly n-srcIndex down to zero-ish for (int trim = 0; srcIndex + trim <= n; trim = 2 * trim + 1) { int length = n - srcIndex - trim; - /* - * System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", - * srcIndex, srcIndex + length, n, destPos, destPos + length, - * actual.length); - */ + + if (verbosity > 1) { + System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", + srcIndex, srcIndex + length, n, destPos, destPos + length, + actual.length); + } + Arrays.fill(actual, BLANK); // Test the method @@ -414,7 +538,7 @@ // Check changed part of destination assertBytesEqual("copyTo(slice) incorrect", test.material.bytes, srcIndex, - actual, destPos, length); + actual, destPos, length); if (destPos > 0) { assertEquals("data before destination", BLANK, actual[destPos - 1]); } @@ -433,8 +557,10 @@ final byte BLANK = 7; for (BufferTestPair test : buffersToWrite) { - System.out.println("copyFrom(): " + test); - PyBuffer view = test.simple; + if (verbosity > 0) { + System.out.println("copyFrom(): " + test); + } + PyBuffer view = test.view; int n = test.material.length; byte[] actual = new byte[n]; @@ -455,9 +581,11 @@ // A variety of lengths from zero to (n-destIndex)-ish for (int length = 0; destIndex + length <= n; length = 2 * length + 1) { - // System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", srcPos, - // srcPos + length, n, destIndex, destIndex + length, - // actual.length); + if (verbosity > 1) { + System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", + srcPos, srcPos + length, n, destIndex, destIndex + length, + actual.length); + } // Initialise the object (have to do each time) and expected value for (int i = 0; i < n; i++) { @@ -480,9 +608,11 @@ for (int trim = 0; destIndex + trim <= n; trim = 2 * trim + 1) { int length = n - destIndex - trim; - // System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", srcPos, - // srcPos + length, n, destIndex, destIndex + length, - // actual.length); + if (verbosity > 1) { + System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", + srcPos, srcPos + length, n, destIndex, destIndex + length, + actual.length); + } // Initialise the object (have to do each time) and expected value for (int i = 0; i < n; i++) { @@ -506,16 +636,351 @@ } /** + * Test method for {@link org.python.core.BufferProtocol#getBuffer()} and + * {@link org.python.core.PyBuffer#getBuffer()}. + */ + public void testGetBuffer() { + + for (BufferTestPair test : buffersToRead) { + if (verbosity > 0) { + System.out.println("getBuffer(): " + test); + } + for (int flags : test.validFlags) { + for (int tassle : test.validTassles) { + PyBuffer view = test.subject.getBuffer(flags | tassle); + assertNotNull(view); + } + } + } + + for (BufferTestPair test : buffersToWrite) { + if (verbosity > 0) { + System.out.println("getBuffer(WRITABLE): " + test); + } + for (int flags : test.validFlags) { + for (int tassle : test.validTassles) { + PyBuffer view = test.subject.getBuffer(flags | tassle | PyBUF.WRITABLE); + assertNotNull(view); + } + } + } + + for (BufferTestPair test : buffersToFailToWrite) { + if (verbosity > 0) { + System.out.println("getBuffer(WRITABLE): " + test); + } + for (int flags : test.validFlags) { + try { + test.subject.getBuffer(flags | PyBUF.WRITABLE); + fail("Write access not prevented: " + test); + } catch (PyException pye) { + // Expect BufferError + assertEquals(Py.BufferError, pye.type); + } + } + } + + } + + /** + * Test method for {@link org.python.core.PyBUF#release()}, exercising the release semantics of + * PyBuffer. + */ + public void testRelease() { + + /* + * Testing the semantics of release() is tricky when it comes to 'final' release behaviour. + * We'd like to test that buffers can be acquired and released, that "over release" is + * detected as an error, and that after final release of the buffer (where the export count + * becomes zero) an exporter remains capable of exporting again. Each test is constructed + * with a subject and a view on the subject (if the subject is an exporter), so you might + * think the export count would be one in every case. Two problems: in many tests, the + * subject is a PyBuffer, which has the option (if it would work) to return itself; and a + * PyBuffer is not expected to provide a new buffer view once finally released. + */ + + Set uniqueBuffers = new HashSet(); + + for (BufferTestPair test : buffersToRead) { + // Test a pattern of acquire and release with one more release than acquire + doTestRelease(test); + uniqueBuffers.add(test.view); + } + + // All buffers are released: test that any further release is detected as an error. + for (PyBuffer view : uniqueBuffers) { + doTestOverRelease(view); + } + + // All exporters are currently not exporting buffers + for (BufferTestPair test : buffersToRead) { + if (!(test.subject instanceof PyBuffer)) { + doTestGetAfterRelease(test); + } + } + } + + /** + * Exercise the release semantics of one BufferTestPair. At the end, the view in the + * BufferTestPair should be fully released, ({@link PyBuffer#isReleased()}==true). + */ + private void doTestRelease(BufferTestPair test) { + + if (verbosity > 0) { + System.out.println("release: " + test); + } + int flags = PyBUF.STRIDES | PyBUF.FORMAT; + BufferProtocol sub = test.subject; + + // The object will be exporting test.view and N other views we don't know about + PyBuffer a = test.view; // = N+1 exports + PyBuffer b = sub.getBuffer(PyBUF.FULL_RO); // = N+2 export + PyBuffer c = sub.getBuffer(flags); // = N+3 exports + checkExporting(sub); + + // Now see that releasing in some other order works correctly + b.release(); // = N+2 exports + a.release(); // = N+1 exports + checkExporting(sub); + + // You can get a buffer from a buffer (c is unreleased) + PyBuffer d = c.getBuffer(flags); // = N+2 exports + c.release(); // = N+1 export + checkExporting(sub); + d.release(); // = N exports + } + + /** + * The view argument should be a fully released buffer, ({@link PyBuffer#isReleased()} + * ==true). We check that further releases raise an error. + */ + private void doTestOverRelease(PyBuffer view) { + + // Was it released finally? + assertTrue("Buffer not finally released as expected", view.isReleased()); + + // Further releases are an error + try { + view.release(); // = -1 exports (oops) + fail("excess release not detected"); + } catch (Exception e) { + // Success + } + + } + + /** + * The test in the argument is one where the subject is a real object (not another buffer) from + * which all buffer views should have been released in {@link #doTestRelease(BufferTestPair)}. + * We check this is true, and that a new buffer may still be acquired from the real object, but + * not from the released buffer. + */ + private void doTestGetAfterRelease(BufferTestPair test) { + + if (verbosity > 0) { + System.out.println("get again: " + test); + } + BufferProtocol sub = test.subject; + + // Fail here if doTestRelease did not fully release, or + checkNotExporting(sub); + + // Further gets via the released buffer are an error + try { + test.view.getBuffer(PyBUF.FULL_RO); + fail("PyBuffer.getBuffer after final release not detected"); + } catch (Exception e) { + // Detected *and* prevented? + checkNotExporting(sub); + } + + // And so are sliced gets + try { + test.view.getBufferSlice(PyBUF.FULL_RO, 0, 0); + fail("PyBuffer.getBufferSlice after final release not detected"); + } catch (Exception e) { + // Detected *and* prevented? + checkNotExporting(sub); + } + + /* + * Even after some abuse, we can still get and release a buffer. + */ + PyBuffer b = sub.getBuffer(PyBUF.FULL_RO); // = 1 export + checkExporting(sub); + b.release(); // = 0 exports + checkNotExporting(sub); + } + + /** + * Error if subject is a PyBuffer and is released, or is a real exporter that (we can tell) is + * not actually exporting. + * + * @param subject + */ + private void checkExporting(BufferProtocol subject) { + if (subject instanceof TestableExporter) { + assertTrue("exports not being counted", ((TestableExporter)subject).isExporting()); + } else if (subject instanceof PyBuffer) { + assertFalse("exports not being counted (PyBuffer)", ((PyBuffer)subject).isReleased()); + } else if (subject instanceof PyByteArray) { + // Size-changing access should fail + try { + ((PyByteArray)subject).bytearray_extend(Py.One); // Appends one zero byte + fail("bytearray_extend with exports should fail"); + } catch (Exception e) { + // Success + } + } + // Other types cannot be checked + } + + /** + * Error if subject is a PyBuffer that is released, or is a real exporter (that we can tell) is + * locked. + * + * @param subject + */ + private void checkNotExporting(BufferProtocol subject) { + if (subject instanceof TestableExporter) { + assertFalse("exports counted incorrectly", ((TestableExporter)subject).isExporting()); + } else if (subject instanceof PyBuffer) { + assertTrue("exports counted incorrectly (PyBuffer)", ((PyBuffer)subject).isReleased()); + } else if (subject instanceof PyByteArray) { + // Size-changing access should succeed + try { + PyByteArray sub = ((PyByteArray)subject); + sub.bytearray_extend(Py.One); + sub.del(sub.__len__() - 1); + } catch (Exception e) { + fail("bytearray unexpectedly locked"); + } + } + // Other types cannot be checked + } + + /** + * Check that reusable PyBuffer is re-used, and that non-reusable PyBuffer is not re-used. + * + * @param subject + */ + private void checkReusable(BufferProtocol subject, PyBuffer previous, PyBuffer latest) { + assertNotNull("Re-used PyBuffer reference null", latest); + if (subject instanceof PyByteArray) { + // Re-use prohibited because might have resized while released + assertFalse("PyByteArray buffer reused unexpectedly", latest == previous); + } else if (subject instanceof TestableExporter && !((TestableExporter)subject).reusable) { + // Special test case where re-use prohibited + assertFalse("PyBuffer reused unexpectedly", latest == previous); + } else { + // Other types of TestableExporter and PyString all re-use + assertTrue("PyBuffer not re-used as expected", latest == previous); + } + } + + /** + * Test method for {@link org.python.core.PyBuffer#getBufferSlice(int, int, int, int)}. + */ + public void testGetBufferSliceWithStride() { + + for (BufferTestPair test : buffersToRead) { + if (verbosity > 0) { + System.out.println("getBufferSliceWithStride: " + test); + } + ByteMaterial material = test.material; + PyBuffer view = test.view; + boolean readonly = test.readonly; + + // Generate some slices from the material and the test view + int N = material.length; + int M = (N + 4) / 4; // At least one and about N/4 + + // For a range of start positions up to one beyond the end + for (int start = 0; start <= N; start += M) { + // For a range of lengths + for (int length : sliceLengths) { + + if (length == 0) { + checkSlice(view, material, start, 0, 1, readonly); + checkSlice(view, material, start, 0, 2, readonly); + + } else if (length == 1 && start < N) { + checkSlice(view, material, start, 1, 1, readonly); + checkSlice(view, material, start, 1, 2, readonly); + + } else if (start < N) { + + // And for a range of step sizes + for (int step : sliceSteps) { + // Check this is a feasible slice + if (start + (length - 1) * step < N) { + 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); + } + } + } + } + } + } + + } + + /** + * 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) { + + int flags = readonly ? PyBUF.FULL_RO : PyBUF.FULL; + + if (verbosity > 1) { + System.out.printf(" checkSlice: start=%4d, length=%4d, step=%4d \n", start, length, + step); + } + 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); + } + + /** * Test method for {@link org.python.core.PyBuffer#getBuf()}. */ public void testGetBuf() { for (BufferTestPair test : buffersToRead) { - System.out.println("getBuf: " + test); - PyBuffer view = test.exporter.getBuffer(PyBUF.SIMPLE); - ByteMaterial m = test.material; + if (verbosity > 0) { + System.out.println("getBuf: " + test); + } + int stride = test.strides[0]; - BufferPointer bp = view.getBuf(); - assertBytesEqual("getBuf: ", m.bytes, bp); + 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); + + BufferPointer 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 + BufferPointer bp = view.getBuf(); + assertBytesEqual("buffer does not match reference", test.material.bytes, bp, stride); + } + } } @@ -524,8 +989,10 @@ */ public void testGetPointer() { for (BufferTestPair test : buffersToRead) { - System.out.println("getPointer: " + test); - PyBuffer view = test.strided; + 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; @@ -538,8 +1005,8 @@ // Get pointer and check contents for correct data BufferPointer bp = view.getPointer(i); - assertBytesEqual("getPointer value", exp, bp.storage, bp.offset); - assertEquals("getPointer size wrong", itemsize, bp.size); + int stride = view.getStrides()[0]; + assertBytesEqual("getPointer value", exp, bp, stride); } } } @@ -550,8 +1017,10 @@ public void testGetPointerNdim() { int[] index = new int[1]; for (BufferTestPair test : buffersToRead) { - System.out.println("getPointer(array): " + test); - PyBuffer view = test.strided; + 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; @@ -566,8 +1035,9 @@ index[0] = i; BufferPointer bp = view.getPointer(index); assertBytesEqual("getPointer value", exp, bp.storage, bp.offset); - assertEquals("getPointer size wrong", itemsize, bp.size); +// assertEquals("getPointer size wrong", itemsize, bp.size); } + // Check 2D index throws try { view.getPointer(0, 0); @@ -580,124 +1050,24 @@ } /** - * Test method for {@link org.python.core.PyBUF#release()}. - */ - public void testRelease() { - for (BufferTestPair test : buffersToRead) { - System.out.println("release: " + test); - BufferProtocol obj = test.exporter; - - // The object should already be exporting test.simple and test.strided = 2 exports - PyBuffer a = test.simple; // 1 exports - PyBuffer b = test.strided; // 2 exports - PyBuffer c = obj.getBuffer(PyBUF.SIMPLE | PyBUF.FORMAT); // = 3 exports - checkExporting(obj); - - // Now see that releasing in some other order works correctly - b.release(); // = 2 exports - a.release(); // = 1 export - checkExporting(obj); - int flags = PyBUF.STRIDES | PyBUF.FORMAT; - - // You can get a buffer from a buffer (for SimpleExporter only c is alive) - PyBuffer d = c.getBuffer(flags); // = 2 exports - c.release(); // = 1 export - checkExporting(obj); - d.release(); // = 0 exports - checkNotExporting(obj); - - // But fails if buffer has been finally released - try { - a = d.getBuffer(flags); // = 0 exports (since disallowed) - fail("getBuffer after final release not detected"); - } catch (Exception e) { - // Detected *and* prevented? - checkNotExporting(obj); - } - - // Further releases are also an error - try { - a.release(); // = -1 exports (oops) - fail("excess release not detected"); - } catch (Exception e) { - // Success - } - - } - } - - /** - * Error if exporter is not actually exporting (and is of a type that locks on export). - * - * @param exporter - */ - private void checkExporting(BufferProtocol exporter) { - if (exporter instanceof TestableExporter) { - assertTrue("exports not being counted", ((TestableExporter)exporter).isExporting()); - } else if (exporter instanceof PyByteArray) { - // Size-changing access should fail - try { - ((PyByteArray)exporter).bytearray_extend(Py.One); // Appends one zero byte - fail("bytearray_extend with exports should fail"); - } catch (Exception e) { - // Success - } - } - // Other types cannot be checked - } - - /** - * Error if exporter is exporting (and is of a type that locks on export). - * - * @param exporter - */ - private void checkNotExporting(BufferProtocol exporter) { - if (exporter instanceof TestableExporter) { - assertFalse("exports falsely counted", ((TestableExporter)exporter).isExporting()); - } else if (exporter instanceof PyByteArray) { - // Size-changing access should fail - try { - ((PyByteArray)exporter).bytearray_extend(Py.One); - } catch (Exception e) { - fail("bytearray unexpectedly locked"); - } - } - // Other types cannot be checked - } - - /** - * Check that reusable PyBuffer is re-used, and that non-reusable PyBuffer is not re-used. - * - * @param exporter - */ - private void checkReusable(BufferProtocol exporter, PyBuffer previous, PyBuffer latest) { - assertNotNull("Re-used PyBuffer reference null", latest); - if (exporter instanceof PyByteArray) { - // Re-use prohibited because might have resized while released - assertFalse("PyByteArray buffer reused unexpectedly", latest == previous); - } else if (exporter instanceof TestableExporter && !((TestableExporter)exporter).reusable) { - // Special test case where re-use prohibited - assertFalse("PyBuffer reused unexpectedly", latest == previous); - } else { - // Other types of TestableExporter and PyString all re-use - assertTrue("PyBuffer not re-used as expected", latest == previous); - } - } - - /** * Test method for {@link org.python.core.PyBUF#getStrides()}. */ public void testGetStrides() { for (BufferTestPair test : buffersToRead) { - System.out.println("getStrides: " + test); - // When not requested ... (different from CPython) - int[] strides = test.simple.getStrides(); - assertNotNull("strides[] should always be provided", strides); - assertIntsEqual("simple.strides", test.strides, strides); - // And when requested, ought to be as expected - strides = test.strided.getStrides(); - assertNotNull("strides[] not provided when requested", strides); - assertIntsEqual("strided.strides", test.strides, strides); + 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); + } + } } } @@ -706,10 +1076,11 @@ */ public void testGetSuboffsets() { for (BufferTestPair test : buffersToRead) { - System.out.println("getSuboffsets: " + test); + if (verbosity > 0) { + System.out.println("getSuboffsets: " + test); + } // Null for all test material - assertNull(test.simple.getSuboffsets()); - assertNull(test.strided.getSuboffsets()); + assertNull(test.view.getSuboffsets()); } } @@ -718,35 +1089,37 @@ */ public void testIsContiguous() { for (BufferTestPair test : buffersToRead) { - System.out.println("isContiguous: " + test); + 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.simple.isContiguous(order)); - assertTrue(orderMsg, test.strided.isContiguous(order)); + assertTrue(orderMsg, test.view.isContiguous(order)); } } } private static final String[] validOrders = {"C-contiguous test fail", - "F-contiguous test fail", - "Any-contiguous test fail"}; + "F-contiguous test fail", "Any-contiguous test fail"}; /** * Test method for {@link org.python.core.PyBuffer#getFormat()}. */ public void testGetFormat() { for (BufferTestPair test : buffersToRead) { - System.out.println("getFormat: " + test); - // When not requested ... (different from CPython) - assertNotNull("format should always be provided", test.simple.getFormat()); - assertNotNull("format should always be provided", test.strided.getFormat()); - // And, we can ask for it explicitly ... - PyBuffer simpleWithFormat = test.exporter.getBuffer(PyBUF.SIMPLE | PyBUF.FORMAT); - PyBuffer stridedWithFormat = test.exporter.getBuffer(PyBUF.STRIDES | PyBUF.FORMAT); - // "B" for all test material where requested in flags - assertEquals("B", simpleWithFormat.getFormat()); - assertEquals("B", stridedWithFormat.getFormat()); + 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()); + } } } @@ -755,20 +1128,39 @@ */ public void testGetItemsize() { for (BufferTestPair test : buffersToRead) { - System.out.println("getItemsize: " + test); + if (verbosity > 0) { + System.out.println("getItemsize: " + test); + } // Unity for all test material - assertEquals(1, test.simple.getItemsize()); - assertEquals(1, test.strided.getItemsize()); + assertEquals(1, test.view.getItemsize()); } } /** - * A class to act as an exporter that uses the SimpleReadonlyBuffer. This permits testing - * abstracted from the Jython interpreter. - *

- * 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). + * Test method for {@link org.python.core.PyBuffer#toString()}. + */ + 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); + } + } + + /* + * ------------------------------------------------------------------------------------------- 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). */ static class SimpleExporter implements BufferProtocol { @@ -795,19 +1187,19 @@ */ static abstract class TestableExporter implements BufferProtocol { - protected Reference export; + protected Reference export; /** * Try to re-use existing exported buffer, or return null if can't. */ - protected PyBuffer getExistingBuffer(int flags) { - PyBuffer pybuf = null; + protected BaseBuffer getExistingBuffer(int flags) { + BaseBuffer pybuf = null; if (export != null) { // A buffer was exported at some time. pybuf = export.get(); if (pybuf != null) { // And this buffer still exists: expect this to provide a further reference - pybuf = pybuf.getBuffer(flags); + pybuf = pybuf.getBufferAgain(flags); } } return pybuf; @@ -842,15 +1234,13 @@ } /** - * A class to act as an exporter that uses the SimpleStringBuffer. This permits testing - * abstracted from the Jython interpreter. - *

- * The exporter shares a single exported buffer between all consumers but does not need to take - * any action when that buffer is finally released. You are most likely to use this approach - * with an exporting object type that does not modify its behaviour while there are active - * exports, but where it is worth avoiding the cost of duplicate buffers. This is the case with - * PyString, where some buffer operations cause construction of a byte array copy of the Java - * String, which it is desirable to do only once. + * A class to act as an exporter that uses the SimpleStringBuffer. The exporter shares a single + * exported buffer between all consumers but does not need to take any action when that buffer + * is finally released. You are most likely to use this approach with an exporting object type + * that does not modify its behaviour while there are active exports, but where it is worth + * avoiding the cost of duplicate buffers. This is the case with PyString, where some buffer + * operations cause construction of a byte array copy of the Java String, which it is desirable + * to do only once. */ static class StringExporter extends TestableExporter { @@ -868,12 +1258,12 @@ @Override public PyBuffer getBuffer(int flags) { // If we have already exported a buffer it may still be available for re-use - PyBuffer pybuf = getExistingBuffer(flags); + BaseBuffer pybuf = getExistingBuffer(flags); if (pybuf == null) { // No existing export we can re-use pybuf = new SimpleStringBuffer(flags, storage); // Hold a reference for possible re-use - export = new SoftReference(pybuf); + export = new SoftReference(pybuf); } return pybuf; } @@ -908,18 +1298,19 @@ @Override public PyBuffer getBuffer(int flags) { // If we have already exported a buffer it may still be available for re-use - PyBuffer pybuf = getExistingBuffer(flags); + BaseBuffer pybuf = getExistingBuffer(flags); if (pybuf == null) { // No existing export we can re-use pybuf = new SimpleWritableBuffer(flags, storage) { + @Override protected void releaseAction() { - export = null; + export = null; // Final release really is final (not reusable) } }; // Hold a reference for possible re-use - export = new WeakReference(pybuf); + export = new WeakReference(pybuf); } return pybuf; } @@ -981,7 +1372,7 @@ } /** Construct from pattern on values (used modulo 256). */ - public ByteMaterial(int start, int inc, int count) { + public ByteMaterial(int start, int count, int inc) { length = count; StringBuilder buf = new StringBuilder(length); bytes = new byte[length]; @@ -1021,6 +1412,53 @@ 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; } /** @@ -1031,27 +1469,21 @@ * @param expected expected byte array * @param bp result to test */ - void assertBytesEqual(String message, byte[] expected, BufferPointer bp) { - int size = bp.size; - if (size != expected.length) { - fail(message + " (size)"); - } else { - int len = bp.storage.length; - if (bp.offset < 0 || bp.offset + size > len) { - fail(message + " (offset)"); - } else { - // Should be safe to compare the bytes - int i = bp.offset, j; - for (j = 0; j < size; j++) { - if (bp.storage[i++] != expected[j]) { - break; - } - } - if (j < size) { - fail(message + " (byte at " + j + ")"); - } - } - } + static void assertBytesEqual(String message, byte[] expected, BufferPointer 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, BufferPointer bp, int stride) { + assertBytesEqual(message, expected, 0, bp.storage, bp.offset, expected.length, stride); } /** @@ -1061,12 +1493,25 @@ * @param expected expected byte array * @param bp result to test */ - void assertBytesEqual(byte[] expected, BufferPointer bp) { + static void assertBytesEqual(byte[] expected, BufferPointer bp) { assertBytesEqual("", expected, bp); } /** - * Customised assert method comparing a byte arrays: values in the actual value starting at + * 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, expected.length, actual.length); + assertBytesEqual(message, expected, 0, actual, 0, expected.length, 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 @@ -1074,13 +1519,13 @@ * @param actual result to test * @param actualStart where to start the comparison in actual */ - void assertBytesEqual(String message, byte[] expected, byte[] actual, int actualStart) { - assertBytesEqual(message, expected, 0, actual, actualStart, expected.length); + static void assertBytesEqual(String message, byte[] expected, byte[] actual, int actualStart) { + assertBytesEqual(message, expected, 0, actual, actualStart, expected.length, 1); } /** - * Customised assert method comparing a byte arrays: values starting at actual[actualStart] must - * those starting at actual[actualStart], for a distance of n bytes. + * 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 @@ -1089,26 +1534,58 @@ * @param actualStart where to start the comparison in actual * @param n number of bytes to test */ - void assertBytesEqual(String message, byte[] expected, int expectedStart, byte[] actual, + static void assertBytesEqual(String message, byte[] expected, int expectedStart, byte[] actual, int actualStart, int n) { - if (actualStart < 0 || expectedStart < 0) { - fail(message + " (start<0)"); - } else if (actualStart + n > actual.length || expectedStart + n > expected.length) { - fail(message + " (too short)"); + + assertBytesEqual(message, expected, expectedStart, actual, actualStart, n, 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 actual result to test + * @param actualStart where to start the comparison in actual + * @param n number of bytes to test + * @param stride spacing of bytes in actual array + */ + static void assertBytesEqual(String message, byte[] expected, int expectedStart, byte[] actual, + int actualStart, int n, 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]) { + 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(Arrays.copyOfRange(actual, actualStart, + actualStart + n))); System.out.println(" _actual_:" + Arrays.toString(actual)); fail(message + " (byte at " + j + ")"); } @@ -1124,7 +1601,7 @@ * @param actual result to test * @param offset where to start the comparison in actual */ - void assertIntsEqual(String message, int[] expected, int[] actual, int offset) { + static void assertIntsEqual(String message, int[] expected, int[] actual, int offset) { int n = expected.length; if (offset < 0) { fail(message + " (offset<0)"); @@ -1154,7 +1631,7 @@ * @param expected expected array * @param actual result to test */ - void assertIntsEqual(String message, int[] expected, int[] actual) { + 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 @@ -1172,55 +1649,134 @@ } /** - * Element for queueing tests, wraps an exporter object with (a copy of) the material from which - * it was created, and several PyBuffer views. + * 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 setUp(), or it may itself be a + * PyBuffer 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. */ - static class BufferTestPair { + 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 subject. If subject 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}; - BufferProtocol exporter; - ByteMaterial material; - PyBuffer simple, strided; - int[] shape, strides; + /** 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 test to do and the material for constructing it (and its results). + * A subject and its reference material, together with explicit shape and strides arrays + * expected. * - * @param exporter - * @param material + * @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 stride 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 exporter, ByteMaterial material, int[] shape, - int[] strides) { - this.exporter = exporter; - this.material = new ByteMaterial(material.ints); + 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; - try { - simple = exporter.getBuffer(PyBUF.SIMPLE); - strided = exporter.getBuffer(PyBUF.STRIDES); - } catch (Exception e) { - // Leave them null if we can't get a PyBuffer: test being set up will fail. - // Silent here, but explicit test of getBuffer will reproduce and log this failure. + 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; } } /** - * A test to do and the material for constructing it (and its results) in one dimension. + * Short constructor for contiguous arrays in one dimension. * - * @param exporter - * @param material + * @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 exporter, ByteMaterial material) { - this(exporter, material, new int[1], STRIDES_1D); + 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; + } + @Override public String toString() { - return exporter.getClass().getSimpleName() + "( " + material.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() + + " )"; } } -- Repository URL: http://hg.python.org/jython From jython-checkins at python.org Sun Nov 25 19:43:00 2012 From: jython-checkins at python.org (jeff.allen) Date: Sun, 25 Nov 2012 19:43:00 +0100 (CET) Subject: [Jython-checkins] =?utf-8?q?jython=3A_Code_style=2C_commentary_ty?= =?utf-8?q?po_and_formatting_change_only=2E?= Message-ID: <3Y8g9w0kshzRXF@mail.python.org> http://hg.python.org/jython/rev/89a429866b3f changeset: 6887:89a429866b3f user: Jeff Allen date: Sun Nov 25 18:01:59 2012 +0000 summary: Code style, commentary typo and formatting change only. files: src/org/python/core/util/StringUtil.java | 34 ++++++------ 1 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/org/python/core/util/StringUtil.java b/src/org/python/core/util/StringUtil.java --- a/src/org/python/core/util/StringUtil.java +++ b/src/org/python/core/util/StringUtil.java @@ -1,12 +1,11 @@ -/* Copyright (c) 2007 Jython Developers */ +/* Copyright (c) 2012 Jython Developers */ package org.python.core.util; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; -import org.python.core.BufferPointer; +import org.python.core.BaseBytes; import org.python.core.Py; -import org.python.core.BaseBytes; import org.python.core.PyBuffer; /** @@ -16,8 +15,8 @@ public class StringUtil { /** - * Encodes this String into a sequence of bytes. Each byte - * contains the low-order bits of its corresponding char. + * Encodes this String into a sequence of bytes. Each byte contains the low-order bits of its + * corresponding char. * * @param string a String value * @return a byte array with one byte for each char in string @@ -25,15 +24,14 @@ public static byte[] toBytes(String string) { try { return string.getBytes("ISO-8859-1"); - } catch(UnsupportedEncodingException uee) { + } catch (UnsupportedEncodingException uee) { // This JVM is whacked, it doesn't even have iso-8859-1 throw Py.SystemError("Java couldn't find the ISO-8859-1 encoding"); } } /** - * Return a new String with chars corresponding to buf from off to - * off + len. + * Return a new String with chars corresponding to buf from off to off + len. * * @param buf an array of bytes * @param off the initial offset @@ -42,7 +40,7 @@ */ @SuppressWarnings("deprecation") public static String fromBytes(byte[] buf, int off, int len) { - // Yes, I known the method is deprecated, but it is the fastest + // Yes, I know the method is deprecated, but it is the fastest // way of converting between between byte[] and String return new String(buf, 0, off, len); } @@ -65,7 +63,7 @@ */ public static String fromBytes(ByteBuffer buf) { return fromBytes(buf.array(), buf.arrayOffset() + buf.position(), - buf.arrayOffset() + buf.limit()); + buf.arrayOffset() + buf.limit()); } /** @@ -88,14 +86,16 @@ */ public static String fromBytes(BaseBytes b) { - int size = b.__len__(); - StringBuilder buf = new StringBuilder(size); - for (int j = 0; j < size; j++) buf.append((char) b.intAt(j)); - return buf.toString(); - } + int size = b.__len__(); + StringBuilder buf = new StringBuilder(size); + for (int j = 0; j < size; j++) { + buf.append((char)b.intAt(j)); + } + return buf.toString(); + } - /** Decapitalize a String if it begins with a capital letter, e.g.: - * FooBar -> fooBar + /** + * Decapitalize a String if it begins with a capital letter, e.g.: FooBar -> fooBar * * @param string a String * @return a decapitalized String -- Repository URL: http://hg.python.org/jython