From jython-checkins at python.org Sat Aug 13 22:19:10 2016 From: jython-checkins at python.org (stefan.richthofer) Date: Sun, 14 Aug 2016 02:19:10 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Fixed_platform=2Emac=5Fver?= =?utf-8?q?=2C_adjusted_os=2Euname_values_on_OSX_to_match?= Message-ID: <20160814021910.68534.89373.14EB97B5@psf.io> https://hg.python.org/jython/rev/63fcb6b8993a changeset: 7924:63fcb6b8993a user: stefan.richthofer date: Sun Aug 14 04:14:04 2016 +0200 summary: Fixed platform.mac_ver, adjusted os.uname values on OSX to match CPython-behavior, adjusted os.uname values on Windows to match behavior of platform.uname in CPython. files: Lib/platform.py | 16 ++ src/org/python/modules/posix/PosixModule.java | 68 ++++++++- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py --- a/Lib/platform.py +++ b/Lib/platform.py @@ -820,6 +820,10 @@ if info is not None: return info + if sys.platform.startswith("java") and _java_getprop("os.name", '') == 'Mac OS X': + res_release = _java_getprop("os.version", release) + return res_release, versioninfo, os.uname()[-1] + # If that also doesn't work return the default values return release,versioninfo,machine @@ -833,6 +837,15 @@ except AttributeError: return default +def _java_getenv(name,default): + try: + value = System.getenv(name) + if value is None: + return default + return newString(value) + except AttributeError: + return default + def java_ver(release='',vendor='',vminfo=('','',''),osinfo=('','','')): """ Version interface for Jython. @@ -1299,6 +1312,9 @@ system = 'Windows' release = 'Vista' + if system == 'Windows' and processor == '': + processor = _java_getenv('PROCESSOR_IDENTIFIER',processor) + _uname_cache = system,node,release,version,machine,processor return _uname_cache diff --git a/src/org/python/modules/posix/PosixModule.java b/src/org/python/modules/posix/PosixModule.java --- a/src/org/python/modules/posix/PosixModule.java +++ b/src/org/python/modules/posix/PosixModule.java @@ -991,13 +991,19 @@ * This version (non-Windows):
* (property os.name, InetAddress.getLocalHost().getHostName(), property os.version, uname -v, uname -m)
*
+ * Adjustments on OSX:
+ * Normalize "Mac OS X" to "Darwin",
+ * uname -r instead of property os.version
+ *
* Fallbacks:
* nodename/uname -n: exec uname -n
* version/uname -v: ""
* machine/uname -m: property os.arch
*
* This version (Windows):
- * (property os.name, InetAddress.getLocalHost().getHostName(), property os.version, cmd.exe /C ver, env PROCESSOR_ARCHITECTURE)
+ * (reproduces platform.uname behavior on Windows)
+ * ("Windows", InetAddress.getLocalHost().getHostName(), property os.name: part after "Windows", + * cmd.exe /C ver: part after "Version", env PROCESSOR_ARCHITECTURE)
*
* Fallback for nodename/uname -n on Windows:
* - env USERDOMAIN
@@ -1006,10 +1012,9 @@ * For machine-entry on Windows this is a simplified description. * It is actually mapped to typical uname -m values as follows (pseudo-code):
*
- * PROCESSOR_ARCHITECTURE = x86 and PROCESSOR_ARCHITEW6432 undefined: "i686"
- * PROCESSOR_ARCHITECTURE = AMD64 or PROCESSOR_ARCHITECTURE = EM64T: "x86_64"
- * PROCESSOR_ARCHITECTURE = IA64: "ia64"
- * else: PROCESSOR_ARCHITECTURE.toLowerCase()
+ * PROCESSOR_ARCHITECTURE = x86 and PROCESSOR_ARCHITEW6432 undefined: "x86"
+ * else if PROCESSOR_ARCHITECTURE = x86: PROCESSOR_ARCHITEW6432 + * else: PROCESSOR_ARCHITECTURE
*
* Potential flaws:
* - could be a 32-bit machine, but actually not i686
@@ -1024,10 +1029,38 @@ if (uname_cache != null) { return uname_cache; } - +// todo: Giving os.uname a windows-implementation might break platform.uname. Check this! String sysname = System.getProperty("os.name"); - String sysrelease = System.getProperty("os.version"); - boolean win = sysname.startsWith("Windows"); + String sysrelease; + boolean win; + if (sysname.equals("Mac OS X")) { + sysname = "Darwin"; + win = false; + try { + Process p = Runtime.getRuntime().exec("uname -r"); + java.io.BufferedReader br = new java.io.BufferedReader( + new java.io.InputStreamReader(p.getInputStream())); + sysrelease = br.readLine(); + // to end the process sanely in case we deal with some + // implementation that emits additional new-lines: + while (br.readLine() != null); + br.close(); + if (p.waitFor() != 0) { + sysrelease = ""; + } + } catch (Exception e) { + sysrelease = ""; + } + } else { + win = sysname.startsWith("Windows"); + if (win) { + sysrelease = sysname.length() > 7 ? sysname.substring(8) : + System.getProperty("os.version"); + sysname = "Windows"; + } else { + sysrelease = System.getProperty("os.version"); + } + } String uname_nodename; try { @@ -1076,6 +1109,17 @@ // No fallback for sysver available uname_sysver = ""; } + if (win && uname_sysver.length() > 0) { + int start = uname_sysver.toLowerCase().indexOf("version "); + if (start != -1) { + start += 8; + int end = uname_sysver.length(); + if (uname_sysver.endsWith("]")) { + --end; + } + uname_sysver = uname_sysver.substring(start, end); + } + } } catch (Exception e) { uname_sysver = ""; } @@ -1088,8 +1132,10 @@ // maybe 32-bit process running on 64 bit machine machine = System.getenv("PROCESSOR_ARCHITEW6432"); } - if (machine == null) { - // if machine == null it's actually a 32-bit machine + // if machine == null it's actually a 32-bit machine + uname_machine = machine == null ? "x86" : machine; +// We refrain from this normalization in order to match platform.uname behavior on Windows: +/* if (machine == null) { uname_machine = "i686"; } else if (machine.equals("AMD64") || machine.equals("EM64T")) { uname_machine = "x86_64"; @@ -1097,7 +1143,7 @@ uname_machine = "ia64"; } else { uname_machine = machine.toLowerCase(); - } + } */ } else { Process p = Runtime.getRuntime().exec("uname -m"); java.io.BufferedReader br = new java.io.BufferedReader( -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Tue Aug 16 09:39:23 2016 From: jython-checkins at python.org (darjus.loktevic) Date: Tue, 16 Aug 2016 13:39:23 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Use_modified_test=5Frobots?= =?utf-8?q?=2Epy_from_1=2E4=2C_as_external_website?= Message-ID: <20160816133921.22202.34280.184163F1@psf.io> https://hg.python.org/jython/rev/03feee033388 changeset: 7925:03feee033388 user: Darjus Loktevic date: Tue Aug 16 23:39:03 2016 +1000 summary: Use modified test_robots.py from 1.4, as external website http://mueblesmoraleda.com/ is no longer with us (http://bugs.python.org/issue20753) files: Lib/test/test_robotparser.py | 317 +++++++++++++++++++++++ 1 files changed, 317 insertions(+), 0 deletions(-) diff --git a/Lib/test/test_robotparser.py b/Lib/test/test_robotparser.py new file mode 100644 --- /dev/null +++ b/Lib/test/test_robotparser.py @@ -0,0 +1,317 @@ +import unittest +import robotparser + +from urllib2 import urlopen, HTTPError +from StringIO import StringIO + +from test import test_support as support +import threading +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + + +class RobotTestCase(unittest.TestCase): + def __init__(self, index=None, parser=None, url=None, good=None, agent=None): + # workaround to make unittest discovery work (see #17066) + if not isinstance(index, int): + return + unittest.TestCase.__init__(self) + if good: + self.str = "RobotTest(%d, good, %s)" % (index, url) + else: + self.str = "RobotTest(%d, bad, %s)" % (index, url) + self.parser = parser + self.url = url + self.good = good + self.agent = agent + + def runTest(self): + if isinstance(self.url, tuple): + agent, url = self.url + else: + url = self.url + agent = self.agent + if self.good: + self.assertTrue(self.parser.can_fetch(agent, url)) + else: + self.assertFalse(self.parser.can_fetch(agent, url)) + + def __str__(self): + return self.str + +tests = unittest.TestSuite() + +def RobotTest(index, robots_txt, good_urls, bad_urls, + agent="test_robotparser"): + + lines = StringIO(robots_txt).readlines() + parser = robotparser.RobotFileParser() + parser.parse(lines) + for url in good_urls: + tests.addTest(RobotTestCase(index, parser, url, 1, agent)) + for url in bad_urls: + tests.addTest(RobotTestCase(index, parser, url, 0, agent)) + +# Examples from http://www.robotstxt.org/wc/norobots.html (fetched 2002) + +# 1. +doc = """ +User-agent: * +Disallow: /cyberworld/map/ # This is an infinite virtual URL space +Disallow: /tmp/ # these will soon disappear +Disallow: /foo.html +""" + +good = ['/','/test.html'] +bad = ['/cyberworld/map/index.html','/tmp/xxx','/foo.html'] + +RobotTest(1, doc, good, bad) + +# 2. +doc = """ +# robots.txt for http://www.example.com/ + +User-agent: * +Disallow: /cyberworld/map/ # This is an infinite virtual URL space + +# Cybermapper knows where to go. +User-agent: cybermapper +Disallow: + +""" + +good = ['/','/test.html',('cybermapper','/cyberworld/map/index.html')] +bad = ['/cyberworld/map/index.html'] + +RobotTest(2, doc, good, bad) + +# 3. +doc = """ +# go away +User-agent: * +Disallow: / +""" + +good = [] +bad = ['/cyberworld/map/index.html','/','/tmp/'] + +RobotTest(3, doc, good, bad) + +# Examples from http://www.robotstxt.org/wc/norobots-rfc.html (fetched 2002) + +# 4. +doc = """ +User-agent: figtree +Disallow: /tmp +Disallow: /a%3cd.html +Disallow: /a%2fb.html +Disallow: /%7ejoe/index.html +""" + +good = [] # XFAIL '/a/b.html' +bad = ['/tmp','/tmp.html','/tmp/a.html', + '/a%3cd.html','/a%3Cd.html','/a%2fb.html', + '/~joe/index.html' + ] + +RobotTest(4, doc, good, bad, 'figtree') +RobotTest(5, doc, good, bad, 'FigTree Robot libwww-perl/5.04') + +# 6. +doc = """ +User-agent: * +Disallow: /tmp/ +Disallow: /a%3Cd.html +Disallow: /a/b.html +Disallow: /%7ejoe/index.html +""" + +good = ['/tmp',] # XFAIL: '/a%2fb.html' +bad = ['/tmp/','/tmp/a.html', + '/a%3cd.html','/a%3Cd.html',"/a/b.html", + '/%7Ejoe/index.html'] + +RobotTest(6, doc, good, bad) + +# From bug report #523041 + +# 7. +doc = """ +User-Agent: * +Disallow: /. +""" + +good = ['/foo.html'] +bad = [] # Bug report says "/" should be denied, but that is not in the RFC + +RobotTest(7, doc, good, bad) + +# From Google: http://www.google.com/support/webmasters/bin/answer.py?hl=en&answer=40364 + +# 8. +doc = """ +User-agent: Googlebot +Allow: /folder1/myfile.html +Disallow: /folder1/ +""" + +good = ['/folder1/myfile.html'] +bad = ['/folder1/anotherfile.html'] + +RobotTest(8, doc, good, bad, agent="Googlebot") + +# 9. This file is incorrect because "Googlebot" is a substring of +# "Googlebot-Mobile", so test 10 works just like test 9. +doc = """ +User-agent: Googlebot +Disallow: / + +User-agent: Googlebot-Mobile +Allow: / +""" + +good = [] +bad = ['/something.jpg'] + +RobotTest(9, doc, good, bad, agent="Googlebot") + +good = [] +bad = ['/something.jpg'] + +RobotTest(10, doc, good, bad, agent="Googlebot-Mobile") + +# 11. Get the order correct. +doc = """ +User-agent: Googlebot-Mobile +Allow: / + +User-agent: Googlebot +Disallow: / +""" + +good = [] +bad = ['/something.jpg'] + +RobotTest(11, doc, good, bad, agent="Googlebot") + +good = ['/something.jpg'] +bad = [] + +RobotTest(12, doc, good, bad, agent="Googlebot-Mobile") + + +# 13. Google also got the order wrong in #8. You need to specify the +# URLs from more specific to more general. +doc = """ +User-agent: Googlebot +Allow: /folder1/myfile.html +Disallow: /folder1/ +""" + +good = ['/folder1/myfile.html'] +bad = ['/folder1/anotherfile.html'] + +RobotTest(13, doc, good, bad, agent="googlebot") + + +# 14. For issue #6325 (query string support) +doc = """ +User-agent: * +Disallow: /some/path?name=value +""" + +good = ['/some/path'] +bad = ['/some/path?name=value'] + +RobotTest(14, doc, good, bad) + +# 15. For issue #4108 (obey first * entry) +doc = """ +User-agent: * +Disallow: /some/path + +User-agent: * +Disallow: /another/path +""" + +good = ['/another/path'] +bad = ['/some/path'] + +RobotTest(15, doc, good, bad) + +# 16. Empty query (issue #17403). Normalizing the url first. +doc = """ +User-agent: * +Allow: /some/path? +Disallow: /another/path? +""" + +good = ['/some/path?'] +bad = ['/another/path?'] + +RobotTest(16, doc, good, bad) + + +class RobotHandler(BaseHTTPRequestHandler): + + def do_GET(self): + self.send_error(403, "Forbidden access") + + def log_message(self, format, *args): + pass + + +class PasswordProtectedSiteTestCase(unittest.TestCase): + + def setUp(self): + self.server = HTTPServer((support.HOST, 0), RobotHandler) + + self.t = threading.Thread( + name='HTTPServer serving', + target=self.server.serve_forever, + # Short poll interval to make the test finish quickly. + # Time between requests is short enough that we won't wake + # up spuriously too many times. + kwargs={'poll_interval':0.01}) + self.t.daemon = True # In case this function raises. + self.t.start() + + def tearDown(self): + self.server.shutdown() + self.t.join() + self.server.server_close() + + def runTest(self): + self.testPasswordProtectedSite() + + def testPasswordProtectedSite(self): + addr = self.server.server_address + url = 'http://' + support.HOST + ':' + str(addr[1]) + robots_url = url + "/robots.txt" + parser = robotparser.RobotFileParser() + parser.set_url(url) + parser.read() + self.assertFalse(parser.can_fetch("*", robots_url)) + + def __str__(self): + return '%s' % self.__class__.__name__ + +class NetworkTestCase(unittest.TestCase): + + @unittest.skip('does not handle the gzip encoding delivered by pydotorg') + def testPythonOrg(self): + support.requires('network') + with support.transient_internet('www.python.org'): + parser = robotparser.RobotFileParser( + "http://www.python.org/robots.txt") + parser.read() + self.assertTrue( + parser.can_fetch("*", "http://www.python.org/robots.txt")) + +def load_tests(loader, suite, pattern): + suite = unittest.makeSuite(NetworkTestCase) + suite.addTest(tests) + suite.addTest(PasswordProtectedSiteTestCase()) + return suite + +if __name__=='__main__': + unittest.main() -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Tue Aug 16 20:23:18 2016 From: jython-checkins at python.org (darjus.loktevic) Date: Wed, 17 Aug 2016 00:23:18 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Eventually_consistent_hack_?= =?utf-8?q?for_looking_up_=5F=5Fgetattribute=5F=5F_from_the_object?= Message-ID: <20160817002318.23348.78997.C2074C68@psf.io> https://hg.python.org/jython/rev/f3458ef83e08 changeset: 7926:f3458ef83e08 user: Darjus Loktevic date: Wed Aug 17 10:23:00 2016 +1000 summary: Eventually consistent hack for looking up __getattribute__ from the object cache. Related to #2487 files: src/org/python/core/Deriveds.java | 14 +++++++++++--- 1 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/org/python/core/Deriveds.java b/src/org/python/core/Deriveds.java --- a/src/org/python/core/Deriveds.java +++ b/src/org/python/core/Deriveds.java @@ -46,12 +46,20 @@ // pass through to __getattr__ } else { PyObject getattribute = type.lookup("__getattribute__"); + // This is a horrible hack for eventual consistency of the cache. We hope that the cached version + // becomes available, but don't wait forever. + for (int i = 0; i < 100000; i++) { + if (getattribute != null) { + break; + } + getattribute = type.lookup("__getattribute__"); + } if (getattribute == null) { // This shouldn't happen - throw Py.SystemError(String.format("__getattribute__ not found on type %s", - type.getName())); + throw Py.SystemError(String.format( + "__getattribute__ not found on type %s. See http://bugs.jython.org/issue2487 for details.", + type.getName())); } - if (getattribute == objectGetattribute) { type.setUsesObjectGetattribute(true); } -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Wed Aug 17 00:38:40 2016 From: jython-checkins at python.org (jim.baker) Date: Wed, 17 Aug 2016 04:38:40 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Update_Netty_to_4=2E1=2E4?= Message-ID: <20160817043840.9446.5178.05046CAC@psf.io> https://hg.python.org/jython/rev/f56990f7a841 changeset: 7928:f56990f7a841 user: Nick Bailey date: Tue Aug 16 22:36:43 2016 -0600 summary: Update Netty to 4.1.4 files: build.xml | 41 +++++----- extlibs/netty-buffer-4.0.33.Final.jar | Bin extlibs/netty-buffer-4.1.4.Final.jar | Bin extlibs/netty-codec-4.0.33.Final.jar | Bin extlibs/netty-codec-4.1.4.Final.jar | Bin extlibs/netty-common-4.0.33.Final.jar | Bin extlibs/netty-common-4.1.4.Final.jar | Bin extlibs/netty-handler-4.0.33.Final.jar | Bin extlibs/netty-handler-4.1.4.Final.jar | Bin extlibs/netty-resolver-4.1.4.Final.jar | Bin extlibs/netty-transport-4.0.33.Final.jar | Bin extlibs/netty-transport-4.1.4.Final.jar | Bin 12 files changed, 22 insertions(+), 19 deletions(-) diff --git a/build.xml b/build.xml --- a/build.xml +++ b/build.xml @@ -33,7 +33,7 @@ may behave unpredictably if called directly. -Where ant looks for ant.properties +Where ant looks for ant.properties ---------------------------------- 1. in user.home 2. in the same directory as this build.xml file @@ -161,11 +161,12 @@ - - - - - + + + + + + @@ -200,7 +201,7 @@ - @@ -501,7 +502,7 @@ source="${jdk.source.version}" debug="${debug}" deprecation="${deprecation}" - nowarn="${nowarn}"> + nowarn="${nowarn}"> @@ -547,7 +548,7 @@ @@ -573,15 +574,17 @@ - + - + - + - + - + + + @@ -673,7 +676,7 @@ + excludes="org/python/expose/generate/**,org/python/version.properties"/> @@ -697,8 +700,8 @@ - - + - + @@ -1173,7 +1176,7 @@ # this configuration was auto-generated by ant build script, # safe to edit by hand (won't be overwritten) java_home="${jdk.home}" -jython_home="${dist.dir}" +jython_home="${dist.dir}" classpath="${dist.dir}/${jython.dev.jar}${path.separator}classes" diff --git a/extlibs/netty-buffer-4.0.33.Final.jar b/extlibs/netty-buffer-4.0.33.Final.jar deleted file mode 100644 index 5e644b4aa8aa4cb736415d71e6a53929fae23005..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 GIT binary patch [stripped] diff --git a/extlibs/netty-buffer-4.1.4.Final.jar b/extlibs/netty-buffer-4.1.4.Final.jar new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5fd2d10dff37034485b8fee065a20d7500223bdc GIT binary patch [stripped] diff --git a/extlibs/netty-codec-4.0.33.Final.jar b/extlibs/netty-codec-4.0.33.Final.jar deleted file mode 100644 index b58013e04bcbcdf8eca12e986b1cd4704cadd1c5..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 GIT binary patch [stripped] diff --git a/extlibs/netty-codec-4.1.4.Final.jar b/extlibs/netty-codec-4.1.4.Final.jar new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6526db4b3ef63b278e930bfc8dab05c8e304958f GIT binary patch [stripped] diff --git a/extlibs/netty-common-4.0.33.Final.jar b/extlibs/netty-common-4.0.33.Final.jar deleted file mode 100644 index b4990abb33b774c96ee0922b2b5505285fade9d5..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 GIT binary patch [stripped] diff --git a/extlibs/netty-common-4.1.4.Final.jar b/extlibs/netty-common-4.1.4.Final.jar new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..44d1d5943d253513797d6ff0820e95ed43ec0a82 GIT binary patch [stripped] diff --git a/extlibs/netty-handler-4.0.33.Final.jar b/extlibs/netty-handler-4.0.33.Final.jar deleted file mode 100644 index ea26491f786d9d4463e1f09a8f7ddebacd9e8763..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 GIT binary patch [stripped] diff --git a/extlibs/netty-handler-4.1.4.Final.jar b/extlibs/netty-handler-4.1.4.Final.jar new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..405779c005975d06e7178deef681374d3587a5d7 GIT binary patch [stripped] diff --git a/extlibs/netty-resolver-4.1.4.Final.jar b/extlibs/netty-resolver-4.1.4.Final.jar new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8033048443ef36621e928e125e5c3f572a4a159a GIT binary patch [stripped] diff --git a/extlibs/netty-transport-4.0.33.Final.jar b/extlibs/netty-transport-4.0.33.Final.jar deleted file mode 100644 index 7be560332f0c25040e9def2f66ee5f7d7b0cec6c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 GIT binary patch [stripped] diff --git a/extlibs/netty-transport-4.1.4.Final.jar b/extlibs/netty-transport-4.1.4.Final.jar new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..865dd416758e1d9a218ea92f626a40744a0f4757 GIT binary patch [stripped] -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Wed Aug 17 00:38:40 2016 From: jython-checkins at python.org (jim.baker) Date: Wed, 17 Aug 2016 04:38:40 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_socket=2Esend_now_checks_ho?= =?utf-8?q?w_many_bytes_can_in_fact_be_sent_without_blocking=2E?= Message-ID: <20160817043840.86260.60885.82CC9A05@psf.io> https://hg.python.org/jython/rev/f7edd144b85a changeset: 7929:f7edd144b85a user: Nick Bailey date: Tue Aug 16 22:38:29 2016 -0600 summary: socket.send now checks how many bytes can in fact be sent without blocking. Fixes #2508 files: Lib/_socket.py | 49 +++++++++++++++++++++---------------- 1 files changed, 28 insertions(+), 21 deletions(-) diff --git a/Lib/_socket.py b/Lib/_socket.py --- a/Lib/_socket.py +++ b/Lib/_socket.py @@ -277,7 +277,7 @@ def java_net_socketexception_handler(exc): if exc.message.startswith("Address family not supported by protocol family"): return _add_exception_attrs( - error(errno.EAFNOSUPPORT, + error(errno.EAFNOSUPPORT, 'Address family not supported by protocol family: See http://wiki.python.org/jython/NewSocketModule#IPV6_address_support')) if exc.message.startswith('Address already in use'): return error(errno.EADDRINUSE, 'Address already in use') @@ -296,7 +296,7 @@ IOException : lambda x: error(errno.ECONNRESET, 'Software caused connection abort'), InterruptedIOException : lambda x: timeout(errno.ETIMEDOUT, 'timed out'), IllegalStateException : lambda x: error(errno.EPIPE, 'Illegal state exception'), - + java.net.BindException : lambda x: error(errno.EADDRINUSE, 'Address already in use'), java.net.ConnectException : lambda x: error(errno.ECONNREFUSED, 'Connection refused'), java.net.NoRouteToHostException : lambda x: error(errno.EHOSTUNREACH, 'No route to host'), @@ -363,7 +363,7 @@ """Maps java socket exceptions to the equivalent python exception. Also sets _last_error on socket objects so as to support SO_ERROR. """ - + @wraps(method_or_function) def handle_exception(*args, **kwargs): is_socket = len(args) > 0 and isinstance(args[0], _realsocket) @@ -457,7 +457,7 @@ class poll(object): - + def __init__(self): self.queue = LinkedBlockingQueue() self.registered = dict() # fd -> eventmask @@ -504,7 +504,7 @@ if notification is None: return None, 0 mask = self.registered.get(notification.fd, 0) # handle if concurrently removed, by simply ignoring - log.debug("Testing notification=%s mask=%s", notification, mask, extra={"sock": "*"}) + log.debug("Testing notification=%s mask=%s", notification, mask, extra={"sock": "*"}) event = 0 if mask & POLLIN and notification.sock._readable(): event |= POLLIN @@ -516,14 +516,14 @@ event |= POLLHUP if mask & POLLNVAL and not notification.sock.peer_closed: event |= POLLNVAL - log.debug("Tested notification=%s event=%s", notification, event, extra={"sock": "*"}) + log.debug("Tested notification=%s event=%s", notification, event, extra={"sock": "*"}) return notification.fd, event def _handle_poll(self, poller): notification = poller() if notification is None: return [] - + # Pull as many outstanding notifications as possible out # of the queue notifications = [notification] @@ -677,7 +677,7 @@ UNKNOWN_SOCKET, CLIENT_SOCKET, SERVER_SOCKET, DATAGRAM_SOCKET = range(4) _socket_types = { UNKNOWN_SOCKET: "unknown", - CLIENT_SOCKET: "client", + CLIENT_SOCKET: "client", SERVER_SOCKET: "server", DATAGRAM_SOCKET: "datagram" } @@ -855,7 +855,7 @@ # in turn use _connect, which uses Bootstrap, not ServerBootstrap def _init_client_mode(self, channel=None): - # this is client socket specific + # this is client socket specific self.socket_type = CLIENT_SOCKET self.incoming = LinkedBlockingQueue() # list of read buffers self.incoming_head = None # allows msg buffers to be broken up @@ -906,7 +906,7 @@ # messages from the peer if self.connect_handlers: self.channel.pipeline().addLast(self.python_inbound_handler) - + def _peer_closed(x): log.debug("Peer closed channel %s", x, extra={"sock": self}) self.channel_closed = True @@ -1016,7 +1016,7 @@ return child, peername # DATAGRAM METHODS - + def _datagram_connect(self, addr=None): # FIXME raise exception if not of the right family if addr is not None: @@ -1077,7 +1077,7 @@ return len(data) # GENERAL METHODS - + def close(self): with self.open_lock: self.open_count -= 1 @@ -1087,7 +1087,7 @@ if self.channel is None: return - + close_future = self.channel.close() close_future.addListener(self._finish_closing) @@ -1179,12 +1179,19 @@ if not self._can_write: raise error(errno.ENOTCONN, 'Socket not connected') - future = self.channel.writeAndFlush(Unpooled.wrappedBuffer(data)) + + bytes_writable = self.channel.bytesBeforeUnwritable() + if bytes_writable > len(data): + bytes_writable = len(data) + + sent_data = data[:bytes_writable] + + future = self.channel.writeAndFlush(Unpooled.wrappedBuffer(sent_data)) self._handle_channel_future(future, "send") - log.debug("Sent data <<<{!r:.20}>>>".format(data), extra={"sock": self}) - # FIXME are we sure we are going to be able to send this much data, especially async? - return len(data) - + log.debug("Sent data <<<{!r:.20}>>>".format(sent_data), extra={"sock": self}) + + return len(sent_data) + sendall = send # FIXME see note above! def _get_incoming_msg(self, reason): @@ -1253,7 +1260,7 @@ data, _ = self._get_message(bufsize, "recv") log.debug("Received <<<{!r:.20}>>>".format(data), extra={"sock": self}) return data - + def recvfrom(self, bufsize, flags=0): self._verify_channel() data, sender = self._get_message(bufsize, "recvfrom") @@ -1438,7 +1445,7 @@ # FIXME handshake_future - gates all requests. should be cheap (comparable to the old self.active) class ChildSocket(_realsocket): - + def __init__(self, parent_socket): super(ChildSocket, self).__init__(type=parent_socket.type) self.parent_socket = parent_socket @@ -1621,7 +1628,7 @@ def _get_jsockaddr2(address_object, family, sock_type, proto, flags): # Is this an object that was returned from getaddrinfo? If so, it already contains an InetAddress if isinstance(address_object, _ip_address_t): - return java.net.InetSocketAddress(address_object.jaddress, address_object[1]) + return java.net.InetSocketAddress(address_object.jaddress, address_object[1]) # The user passed an address tuple, not an object returned from getaddrinfo # So we must call getaddrinfo, after some translations and checking if address_object is None: -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Wed Aug 17 00:38:39 2016 From: jython-checkins at python.org (jim.baker) Date: Wed, 17 Aug 2016 04:38:39 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Perform_SSL_handshake=2C_re?= =?utf-8?q?gardless_if_original_socket_is_wrapped=2E_Fixes_=232462?= Message-ID: <20160817043839.2434.51317.EABA2349@psf.io> https://hg.python.org/jython/rev/aea64acc5e47 changeset: 7927:aea64acc5e47 user: Jim Baker date: Tue Aug 16 22:03:56 2016 -0600 summary: Perform SSL handshake, regardless if original socket is wrapped. Fixes #2462 files: Lib/_socket.py | 139 +++++++++++++++--------- Lib/_sslcerts.py | 5 +- Lib/ssl.py | 140 +++++++++++++++++------- Lib/test/regrtest.py | 17 +- Lib/test/test_socket_jy.py | 7 +- 5 files changed, 197 insertions(+), 111 deletions(-) diff --git a/Lib/_socket.py b/Lib/_socket.py --- a/Lib/_socket.py +++ b/Lib/_socket.py @@ -21,7 +21,7 @@ import java from java.io import IOException, InterruptedIOException -from java.lang import Thread, IllegalStateException +from java.lang import Thread, ArrayIndexOutOfBoundsException, IllegalStateException from java.net import InetAddress, InetSocketAddress from java.nio.channels import ClosedChannelException from java.security.cert import CertificateException @@ -41,6 +41,8 @@ from org.python.netty.channel.nio import NioEventLoopGroup from org.python.netty.channel.socket import DatagramPacket from org.python.netty.channel.socket.nio import NioDatagramChannel, NioSocketChannel, NioServerSocketChannel + from org.python.netty.handler.ssl import NotSslRecordException + except ImportError: # dev version from extlibs from io.netty.bootstrap import Bootstrap, ChannelFactory, ServerBootstrap @@ -49,7 +51,7 @@ from io.netty.channel.nio import NioEventLoopGroup from io.netty.channel.socket import DatagramPacket from io.netty.channel.socket.nio import NioDatagramChannel, NioSocketChannel, NioServerSocketChannel - + from io.netty.handler.ssl import NotSslRecordException log = logging.getLogger("_socket") log.setLevel(level=logging.WARNING) @@ -248,7 +250,7 @@ class herror(error): pass class gaierror(error): pass class timeout(error): pass -class SSLError(error): pass +class SSLError(error): pass # FIXME import from ssl, solving the usual mutual import schema SSL_ERROR_SSL = 1 SSL_ERROR_WANT_READ = 2 @@ -259,6 +261,7 @@ SSL_ERROR_WANT_CONNECT = 7 SSL_ERROR_EOF = 8 SSL_ERROR_INVALID_ERROR_CODE = 9 +SSL_UNKNOWN_PROTOCOL = 10 # FIXME check code from OpenSSL def _add_exception_attrs(exc): @@ -321,6 +324,12 @@ java.nio.channels.UnsupportedAddressTypeException : None, SSLPeerUnverifiedException: lambda x: SSLError(SSL_ERROR_SSL, x.message), + # FIXME + # CPython wraps with a message like so: + # ssl.SSLError: [SSL: UNKNOWN_PROTOCOL] unknown protocol (_ssl.c:590) + # Currently this error handler produces this message: + # _socket.SSLError: [Errno 1] not an SSL/TLS record: 48692c2049276d206120636c69656e7421 + NotSslRecordException: lambda x: SSLError(SSL_UNKNOWN_PROTOCOL, x.message), } @@ -605,10 +614,11 @@ child.proto = IPPROTO_TCP child._init_client_mode(child_channel) - # Get most current options from the parent. This enables any subsequent divergence. + # Get most current options from the parent. This enables any + # subsequent divergence. # - # It's OK that this copy could occur without a mutex, given that such iteration - # is guaranteed to be weakly consistent + # It's OK that this copy could occur without a mutex, given + # that such iteration is guaranteed to be weakly consistent child.options = dict(((option, value) for option, value in self.parent_socket.options.iteritems())) if child.options: log.debug("Setting inherited options %s", child.options, extra={"sock": child}) @@ -616,29 +626,51 @@ for option, value in child.options.iteritems(): _set_option(config.setOption, option, value) - log.debug("Notifing listeners of parent socket %s", self.parent_socket, extra={"sock": child}) - self.parent_socket.child_queue.put(child) - self.parent_socket._notify_selectors() - log.debug("Notified listeners of parent socket %s with queue %s", - self.parent_socket, self.parent_socket.child_queue, extra={"sock": child}) + # Ensure that this handler will not block if the channel is + # closed, otherwise this handler will simply sit idly as a + # pending task in the Netty thread pool + child_channel.closeFuture().addListener(child._make_active) - # Must block until the child socket is actually "used". This is - # because there may be some additional setup required, such as - # wrapping the socket, before the child is ready to read. + # Parent socket is wrapping, so we know intent and we can + # return as soon as handshaking is completed, if any + if hasattr(self.parent_socket, "ssl_wrap_child_socket"): + log.debug("Wrapping child socket for a wrapped parent=%s", self.parent_socket, extra={"sock": self}) + child._wrapper_socket = self.parent_socket.ssl_wrap_child_socket(child) + child._handshake_future.sync() + child._post_connect() + self.parent_socket.child_queue.put(child) + log.debug("Notifing listeners of parent socket %s", self.parent_socket, extra={"sock": child}) + self.parent_socket._notify_selectors() + log.debug("Notified listeners of parent socket %s with queue %s", + self.parent_socket, self.parent_socket.child_queue, extra={"sock": child}) + return - def unlatch_child(_): - # FIXME when bound methods are supported for single method interfaces - child._unlatch() + # Otherwise, must wait on this barrier until the child socket + # is activated, as demonstrated by use or other setup + # info. This is because the child may still be OPTIONALLY + # wrapped with an SSL socket. Not blocking here will cause + # corruption in send/recv data because it will overlap with + # the handshaking in that case. + with child._activation_cv: + def wait_for_barrier(): + with child._activation_cv: + self.parent_socket.child_queue.put(child) + log.debug("Notifing listeners of parent socket %s", self.parent_socket, extra={"sock": child}) + self.parent_socket._notify_selectors() + log.debug("Notified listeners of parent socket %s with queue %s", + self.parent_socket, self.parent_socket.child_queue, extra={"sock": child}) + self.parent_socket.parent_group.submit(wait_for_barrier) + while not child._activated: + log.debug("Waiting for optional wrapping", extra={"sock": child}) + child._activation_cv.wait() - # Ensure that this handler will not block if the channel is closed, - # otherwise this handler will simply sit idly as a pending task in the Netty - # thread pool - child_channel.closeFuture().addListener(unlatch_child) - - if self.parent_socket.timeout is None: - child._ensure_post_connect() - child._wait_on_latch() - log.debug("Socket initChannel completed waiting on latch", extra={"sock": child}) + log.debug("Completed waiting for optional wrapping", extra={"sock": child}) + if hasattr(child, "ssl_wrap_self"): + log.debug("Wrapping self", extra={"sock": child}) + child.ssl_wrap_self() + log.debug("Activating child socket by adding inbound handler", extra={"sock": child}) + child._post_connect() + child._channel_is_initialized = True # FIXME raise exceptions for ops not permitted on client socket, server socket @@ -651,8 +683,6 @@ } - - def _identity(value): return value @@ -725,7 +755,6 @@ self.timeout = _defaulttimeout self.channel = None self.bind_addr = _EPHEMERAL_ADDRESS - self.bind_timestamp = None # Handle Netty race condition on bound addresses self.selectors = CopyOnWriteArrayList() self.options = {} # deferred options until bootstrap self.peer_closed = False @@ -748,10 +777,11 @@ return "<_realsocket at {:#x} type={} open_count={} channel={} timeout={}>".format( id(self), _socket_types[self.socket_type], self.open_count, self.channel, self.timeout) - def _unlatch(self): - pass # no-op once mutated from ChildSocket to normal _socketobject + def _make_active(self): + pass def _register_selector(self, selector): + self._make_active() # attempting to poll/select on a socket means waiting for wrap intent is done self.selectors.addIfAbsent(selector) def _unregister_selector(self, selector): @@ -759,6 +789,8 @@ return self.selectors.remove(selector) except ValueError: return None + except ArrayIndexOutOfBoundsException: + return None def _notify_selectors(self, exception=None, hangup=False): for selector in self.selectors: @@ -867,7 +899,6 @@ self.connect_future = self.channel.connect(addr) self._handle_channel_future(self.connect_future, "connect") - self.bind_timestamp = time.time() def _post_connect(self): # Post-connect step is necessary to handle SSL setup, @@ -882,6 +913,7 @@ self.incoming.put(_PEER_CLOSED) self._notify_selectors(hangup=True) + log.debug("Add _peer_closed to channel close", extra={"sock": self}) self.channel.closeFuture().addListener(_peer_closed) def connect(self, addr): @@ -906,7 +938,7 @@ if was_connecting: try: # Timing is based on CPython and was empirically - # guestimated. Of course this means user code is + # guesstimated. Of course this means user code is # polling, so the the best we can do is wait like # this in supposedly nonblocking mode without # completely busy waiting! @@ -961,7 +993,6 @@ self.bind_future = b.bind(self.bind_addr.getAddress(), self.bind_addr.getPort()) self._handle_channel_future(self.bind_future, "listen") - self.bind_timestamp = time.time() self.channel = self.bind_future.channel() log.debug("Bound server socket to %s", self.bind_addr, extra={"sock": self}) @@ -1157,6 +1188,7 @@ sendall = send # FIXME see note above! def _get_incoming_msg(self, reason): + log.debug("head=%s incoming=%s" % (self.incoming_head, self.incoming), extra={"sock": self}) if self.incoming_head is None: if self.timeout is None: if self.peer_closed: @@ -1403,28 +1435,25 @@ socket = SocketType = _socketobject +# FIXME handshake_future - gates all requests. should be cheap (comparable to the old self.active) + class ChildSocket(_realsocket): def __init__(self, parent_socket): super(ChildSocket, self).__init__(type=parent_socket.type) self.parent_socket = parent_socket - self.active = AtomicBoolean() - self.active_latch = CountDownLatch(1) + self._activation_cv = Condition() + self._activated = False self.accepted = False self.timeout = parent_socket.timeout - def _ensure_post_connect(self): - do_post_connect = not self.active.getAndSet(True) - if do_post_connect: - if hasattr(self.parent_socket, "ssl_wrap_child_socket"): - self.parent_socket.ssl_wrap_child_socket(self) - self._post_connect() - self.active_latch.countDown() - - def _wait_on_latch(self): - log.debug("Waiting for activity", extra={"sock": self}) - self.active_latch.await() - log.debug("Latch released, can now proceed", extra={"sock": self}) + def _make_active(self, *ignore): # ignore result arg when used as a listener on a future + if self._activated: + return + with self._activation_cv: + self._activated = True + self._activation_cv.notify() + log.debug("Child socket is now activated", extra={"sock": self}) # FIXME raise exception for accept, listen, bind, connect, connect_ex @@ -1434,25 +1463,25 @@ # connection, not metadata. def send(self, data): - self._ensure_post_connect() + self._make_active() return super(ChildSocket, self).send(data) sendall = send def recv(self, bufsize, flags=0): - self._ensure_post_connect() + self._make_active() return super(ChildSocket, self).recv(bufsize, flags) def recvfrom(self, bufsize, flags=0): - self._ensure_post_connect() + self._make_active() return super(ChildSocket, self).recvfrom(bufsize, flags) def setblocking(self, mode): - self._ensure_post_connect() + self._make_active() return super(ChildSocket, self).setblocking(mode) def close(self): - self._ensure_post_connect() + self._make_active() super(ChildSocket, self).close() if self.open_count > 0: return @@ -1465,7 +1494,7 @@ self.parent_socket.child_group.shutdownGracefully(0, 100, TimeUnit.MILLISECONDS) def shutdown(self, how): - self._ensure_post_connect() + self._make_active() super(ChildSocket, self).shutdown(how) def __del__(self): @@ -1474,7 +1503,7 @@ # handler is released when a GC happens, not necessarily # before shutdown of course. Naturally no extra work will be # done in setting up the channel. - self.active_latch.countDown() + self._make_active() self.close() diff --git a/Lib/_sslcerts.py b/Lib/_sslcerts.py --- a/Lib/_sslcerts.py +++ b/Lib/_sslcerts.py @@ -24,8 +24,9 @@ # http://bugs.jython.org/issue2469, due to the fact that jarjar-ed # jars - like other shading - lose their signatures. For most jars # this is not an issue, and we have been removing signature files - # since 2.7.0, but it causes conflicts Java's security provider - # model. + # since 2.7.0. But in this specific case, removing signatures then + # causes conflicts with Java's security provider model, because it + # requires signing. from org.bouncycastle.asn1.pkcs import PrivateKeyInfo from org.bouncycastle.cert import X509CertificateHolder from org.bouncycastle.cert.jcajce import JcaX509CertificateConverter diff --git a/Lib/ssl.py b/Lib/ssl.py --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -52,6 +52,7 @@ from java.util import ArrayList, Locale, TimeZone, NoSuchElementException from java.util.concurrent import CountDownLatch from javax.naming.ldap import LdapName +from javax.net.ssl import SSLException, SSLHandshakeException from javax.security.auth.x500 import X500Principal from org.ietf.jgss import Oid @@ -393,7 +394,7 @@ All Python stdlib modules shall use this function to create SSLContext objects in order to keep common settings in one place. The configuration - is less restrict than create_default_context()'s to increase backward + is less restricted than create_default_context()'s to increase backward compatibility. """ if not isinstance(purpose, _ASN1Object): @@ -443,17 +444,6 @@ pipeline = ch.pipeline() pipeline.addFirst("ssl", self.ssl_handler) -class RaceFreeSslHandler(SslHandler): - """ - This is a temporary workaround to solve a race condition that is present in - Netty 4.0.33. The race condition causes an NPE because 'this.ctx' isn't set when - calling channelActive. Once we upgrade to a version of Netty that fixes the race - condition, we should remove this. - """ - - def channelActive(self, ctx): - self.ctx = ctx - SslHandler.channelActive(self) class SSLSocket(object): @@ -465,6 +455,14 @@ self.sock = sock self.do_handshake_on_connect = do_handshake_on_connect self._sock = sock._sock # the real underlying socket + + # FIXME in CPython, a check like so is performed - but this is + # not quite correct, based on tests. We should revisit to see + # if we can make this work as desired. + + # if do_handshake_on_connect and self._sock.timeout == 0: + # raise ValueError("do_handshake_on_connect should not be specified for non-blocking sockets") + self._connected = False if _context: self._context = _context @@ -507,30 +505,72 @@ self.ssl_handler = None # We use _sslobj here to support the CPython convention that - # an object means we have handshaked, as used by existing code - # in the wild that looks at this ostensibly internal attribute - self._sslobj = None - self.handshake_count = 0 + # an object means we have handshaked. It is used by existing code + # in the wild that looks at this ostensibly internal attribute. + + # FIXME CPython uses _sslobj to track the OpenSSL wrapper + # object that's implemented in C, with the following + # properties: + # + # 'cipher', 'compression', 'context', 'do_handshake', + # 'peer_certificate', 'pending', 'read', 'shutdown', + # 'tls_unique_cb', 'version', 'write' + self._sslobj = self # setting to self is not quite right self.engine = None if self.do_handshake_on_connect and self._sock.connected: + log.debug("Handshaking socket on connect", extra={"sock": self._sock}) if isinstance(self._sock, ChildSocket): - log.debug("Child socket - do not handshake! type=%s parent=%s", type(self._sock), self._sock.parent_socket, - extra={"sock": self._sock}) + # Need to handle child sockets differently depending + # on whether the parent socket is wrapped or not. + # + # In either case, we cannot handshake here in this + # thread - it must be done in the child pool and + # before the child is activated. + # + # 1. If wrapped, this is going through SSLSocket.accept + + if isinstance(self._sock.parent_socket, SSLSocket): + # already wrapped, via `wrap_child` function a few lines below + log.debug( + "Child socket - will handshake in child loop type=%s parent=%s", + type(self._sock), self._sock.parent_socket, + extra={"sock": self._sock}) + self._sock._make_active() + + # 2. If not, using code will be calling SSLContext.wrap_socket + # *after* accept from an unwrapped socket + + else: + log.debug("Child socket will wrap self with handshake", extra={"sock": self._sock}) + setup_handshake_latch = CountDownLatch(1) + + def setup_handshake(): + handshake_future = self.do_handshake() + setup_handshake_latch.countDown() + return handshake_future + + self._sock.ssl_wrap_self = setup_handshake + self._sock._make_active() + setup_handshake_latch.await() + log.debug("Child socket waiting on handshake=%s", self._handshake_future, extra={"sock": self._sock}) + self._sock._handle_channel_future(self._handshake_future, "SSL handshake") else: self.do_handshake() if hasattr(self._sock, "accepted_children"): def wrap_child(child): - log.debug("Wrapping child socket - about to handshake! parent=%s", self._sock, extra={"sock": child}) + log.debug( + "Wrapping child socket - about to handshake! parent=%s", + self._sock, extra={"sock": child}) child._wrapper_socket = self.context.wrap_socket( _socketobject(_sock=child), do_handshake_on_connect=self.do_handshake_on_connect, suppress_ragged_eofs=self.suppress_ragged_eofs, server_side=True) - if self.do_handshake_on_connect: + # this handshake will be done in the child pool - initChannel will block on it child._wrapper_socket.do_handshake() self._sock.ssl_wrap_child_socket = wrap_child @@ -583,15 +623,21 @@ SSL channel, and the address of the remote client.""" child, addr = self._sock.accept() if self.do_handshake_on_connect: - child.active_latch.await() - - log.debug("accepted sock=%s wrapped=%s addr=%s", child, child._wrapper_socket, addr, extra={"sock": self._sock}) - wrapped_child_socket = child._wrapper_socket - del child._wrapper_socket - return wrapped_child_socket, addr + wrapped_child_socket = child._wrapper_socket + del child._wrapper_socket + return wrapped_child_socket, addr + else: + return self.context.wrap_socket( + _socketobject(_sock=child), + do_handshake_on_connect=self.do_handshake_on_connect, + suppress_ragged_eofs=self.suppress_ragged_eofs, + server_side=True) def unwrap(self): - self._sock.channel.pipeline().remove("ssl") + try: + self._sock.channel.pipeline().remove("ssl") + except NoSuchElementException: + pass self.ssl_handler.close() return self._sock @@ -601,16 +647,10 @@ def handshake_step(result): log.debug("SSL handshaking completed %s", result, extra={"sock": self._sock}) - - if not hasattr(self._sock, "active_latch"): - log.debug("Post connect step", extra={"sock": self._sock}) - self._sock._post_connect() - self._sock._unlatch() - self._sslobj = object() # we have now handshaked self._notify_selectors() if self.ssl_handler is None: - self.ssl_handler = RaceFreeSslHandler(self.engine) + self.ssl_handler = SslHandler(self.engine) self.ssl_handler.handshakeFuture().addListener(handshake_step) if hasattr(self._sock, "connected") and self._sock.connected: @@ -621,38 +661,52 @@ log.debug("Not connected, adding SSL initializer...", extra={"sock": self._sock}) self._sock.connect_handlers.append(SSLInitializer(self.ssl_handler)) - handshake = self.ssl_handler.handshakeFuture() - time.sleep(0.001) # Necessary apparently for the handler to get into a good state + self._handshake_future = self.ssl_handler.handshakeFuture() if isinstance(self._sock, ChildSocket): + pass # see # http://stackoverflow.com/questions/24628271/exception-in-netty-io-netty-util-concurrent-blockingoperationexception - # - we are doing this in the handler thread! - return - - self._sock._handle_channel_future(handshake, "SSL handshake") + # - handshake in the child thread pool + else: + self._sock._handle_channel_future(self._handshake_future, "SSL handshake") def dup(self): raise NotImplemented("Can't dup() %s instances" % self.__class__.__name__) + @raises_java_exception + def _ensure_handshake(self): + log.debug("Ensure handshake", extra={"sock": self}) + self._sock._make_active() + # nonblocking code should never wait here, but only attempt to + # come to this point when notified via a selector + if not hasattr(self, "_handshake_future"): + self.do_handshake() + # additional synchronization guard if this is a child socket + self._handshake_future.sync() + log.debug("Completed post connect", extra={"sock": self}) + # Various pass through methods to the wrapped socket def send(self, data): + self._ensure_handshake() return self.sock.send(data) write = send def sendall(self, data): + self._ensure_handshake() return self.sock.sendall(data) def recv(self, bufsize, flags=0): + self._ensure_handshake() return self.sock.recv(bufsize, flags) def read(self, len=0, buffer=None): """Read up to LEN bytes and return them. Return zero-length string on EOF.""" - self._checkClosed() + self._ensure_handshake() # FIXME? breaks test_smtpnet.py # if not self._sslobj: # raise ValueError("Read on closed or unwrapped SSL socket.") @@ -672,17 +726,21 @@ raise def recvfrom(self, bufsize, flags=0): + self._ensure_handshake() return self.sock.recvfrom(bufsize, flags) def recvfrom_into(self, buffer, nbytes=0, flags=0): + self._ensure_handshake() return self.sock.recvfrom_into(buffer, nbytes, flags) def recv_into(self, buffer, nbytes=0, flags=0): + self._ensure_handshake() return self.sock.recv_into(buffer, nbytes, flags) def sendto(self, string, arg1, arg2=None): # as observed on CPython, sendto when wrapped ignores the # destination address, thereby behaving just like send + self._ensure_handshake() return self.sock.send(string) def close(self): diff --git a/Lib/test/regrtest.py b/Lib/test/regrtest.py --- a/Lib/test/regrtest.py +++ b/Lib/test/regrtest.py @@ -1313,7 +1313,6 @@ # fails on Windows standalone, probably shouldn't test_netrc # KeyError: 'foo.domain.com' - test_shutil # Operation not permitted errors test_zipfile # fails on Windows standalone too, but more embarassing as java specific @@ -1336,18 +1335,16 @@ # Unreliable tests test_asynchat - test_gc # Rare failures depending on timing of Java gc - test_logging - test_select_new - test_socket # flakey (Windows) + # test_gc # Rare failures depending on timing of Java gc + # test_logging test_tarfile # flakey (Windows) - test_threading - test_urllib2net # unexpected output makes this a failure to regrtest.py + # test_urllib2net # unexpected output makes this a failure to regrtest.py - # Tests that should work with socket-reboot, but currently fail/hang - test_ftplib # NoSuchElementException ssl + # Failing tests here are because of lack of STARTTLS; see http://bugs.jython.org/issue2447 + # (which produces "'NoneType' is not iterable" in the server accept loop) + test_ftplib test_httplib - test_poplib # 'NoneType' is not iterable + test_poplib test_smtplib # Problems with the latest JSR 223 changes; see http://bugs.jython.org/issue2154 diff --git a/Lib/test/test_socket_jy.py b/Lib/test/test_socket_jy.py --- a/Lib/test/test_socket_jy.py +++ b/Lib/test/test_socket_jy.py @@ -51,11 +51,11 @@ connect_errno = 0 connect_attempt = 0 - while connect_errno != errno.EISCONN and connect_attempt < 5000: + while connect_errno != errno.EISCONN and connect_attempt < 10000: connect_attempt += 1 connect_errno = sock.connect_ex(self.address) results[index].append(connect_errno) - time.sleep(0.001) + time.sleep(0.01) sock.close() def do_workout(self, num_threads=10): @@ -108,10 +108,11 @@ connect_attempt = 0 sock = ssl.wrap_socket(sock, certfile=CERTFILE, do_handshake_on_connect=True) - while connect_errno != errno.EISCONN and connect_attempt < 500: + while connect_errno != errno.EISCONN and connect_attempt < 10000: connect_attempt += 1 connect_errno = sock.connect_ex(self.address) results[index].append(connect_errno) + time.sleep(0.01) sock.close() def do_workout(self, num_threads=10): -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Wed Aug 17 12:53:46 2016 From: jython-checkins at python.org (stefan.richthofer) Date: Wed, 17 Aug 2016 16:53:46 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Let_PySequenceList_implemen?= =?utf-8?q?t_java=2Eutil=2EList=2E?= Message-ID: <20160817165345.11620.84895.64D4A81A@psf.io> https://hg.python.org/jython/rev/69f0e51cf290 changeset: 7930:69f0e51cf290 user: stefan.richthofer date: Wed Aug 17 18:53:22 2016 +0200 summary: Let PySequenceList implement java.util.List. files: src/org/python/core/PyList.java | 2 +- src/org/python/core/PySequenceList.java | 2 +- src/org/python/core/PyTuple.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/org/python/core/PyList.java b/src/org/python/core/PyList.java --- a/src/org/python/core/PyList.java +++ b/src/org/python/core/PyList.java @@ -23,7 +23,7 @@ import java.util.Map; @ExposedType(name = "list", base = PyObject.class, doc = BuiltinDocs.list_doc) -public class PyList extends PySequenceList implements List { +public class PyList extends PySequenceList { public static final PyType TYPE = PyType.fromClass(PyList.class); { diff --git a/src/org/python/core/PySequenceList.java b/src/org/python/core/PySequenceList.java --- a/src/org/python/core/PySequenceList.java +++ b/src/org/python/core/PySequenceList.java @@ -5,7 +5,7 @@ import java.util.List; import java.util.ListIterator; -public abstract class PySequenceList extends PySequence implements Traverseproc { +public abstract class PySequenceList extends PySequence implements List, Traverseproc { protected PySequenceList(PyType type) { super(type); diff --git a/src/org/python/core/PyTuple.java b/src/org/python/core/PyTuple.java --- a/src/org/python/core/PyTuple.java +++ b/src/org/python/core/PyTuple.java @@ -18,7 +18,7 @@ * A builtin python tuple. */ @ExposedType(name = "tuple", base = PyObject.class, doc = BuiltinDocs.tuple_doc) -public class PyTuple extends PySequenceList implements List { +public class PyTuple extends PySequenceList { public static final PyType TYPE = PyType.fromClass(PyTuple.class); -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Wed Aug 17 22:30:12 2016 From: jython-checkins at python.org (stefan.richthofer) Date: Thu, 18 Aug 2016 02:30:12 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Restored_platform=2Euname_t?= =?utf-8?q?o_behave_like_before_os=2Euname_was_added_=28With_one?= Message-ID: <20160818023012.9540.4300.2409C884@psf.io> https://hg.python.org/jython/rev/55ce051b550c changeset: 7931:55ce051b550c user: stefan.richthofer date: Thu Aug 18 04:29:32 2016 +0200 summary: Restored platform.uname to behave like before os.uname was added (With one improvement: machine is now filled in). The previous behavior (similar to CPython) is preserved as platform._uname (naming consistent to os._name vs os.name). files: Lib/platform.py | 66 +++++++++++++++++++++++++++++++++++- 1 files changed, 63 insertions(+), 3 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1191,15 +1191,75 @@ Entries which cannot be determined are set to ''. + + Jython-note: + platform.uname returns JVM-info. + For native platform info use os.uname or platform._uname. """ global _uname_cache - no_os_uname = 0 if _uname_cache is not None: return _uname_cache processor = '' + node = _node() + machine = os.uname()[4] + + # Get JVM-Info: + release,vendor,vminfo,osinfo = java_ver() + system = 'Java' + version = string.join(vminfo,', ') + if not version: + version = vendor + + if not processor: + # Get processor information from the uname system command + processor = _syscmd_uname('-p','') + + #If any unknowns still exist, replace them with ''s, which are more portable + if system == 'unknown': + system = '' + if node == 'unknown': + node = '' + if release == 'unknown': + release = '' + if version == 'unknown': + version = '' + if machine == 'unknown': + machine = '' + if processor == 'unknown': + processor = '' + + _uname_cache = system,node,release,version,machine,processor + return _uname_cache + + +# We preserve original uname (more or less) here as _uname: +__uname_cache = None + +def _uname(): + + """ Fairly portable uname interface. Returns a tuple + of strings (system,node,release,version,machine,processor) + identifying the underlying platform. + + Note that unlike the os.uname function this also returns + possible processor information as an additional tuple entry. + + Entries which cannot be determined are set to ''. + + Jython-note: + _uname resembles CPython behavior for debugging purposes etc. + """ + global __uname_cache + no_os_uname = 0 + + if __uname_cache is not None: + return __uname_cache + + processor = '' + # Get some infos from the builtin os.uname API... try: system,node,release,version,machine = os.uname() @@ -1315,8 +1375,8 @@ if system == 'Windows' and processor == '': processor = _java_getenv('PROCESSOR_IDENTIFIER',processor) - _uname_cache = system,node,release,version,machine,processor - return _uname_cache + __uname_cache = system,node,release,version,machine,processor + return __uname_cache ### Direct interfaces to some of the uname() return values -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Mon Aug 22 21:10:20 2016 From: jython-checkins at python.org (stefan.richthofer) Date: Tue, 23 Aug 2016 01:10:20 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Minor_improvements_in_uname?= =?utf-8?q?_in_Lib/platform=2Epy=2E?= Message-ID: <20160823011019.5881.77412.48E05C6A@psf.io> https://hg.python.org/jython/rev/7635c5999b8e changeset: 7932:7635c5999b8e user: stefan.richthofer date: Tue Aug 23 03:08:27 2016 +0200 summary: Minor improvements in uname in Lib/platform.py. files: Lib/platform.py | 14 +++++++------- 1 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/platform.py b/Lib/platform.py --- a/Lib/platform.py +++ b/Lib/platform.py @@ -1215,7 +1215,7 @@ if not processor: # Get processor information from the uname system command - processor = _syscmd_uname('-p','') + processor = _uname()[-1] #If any unknowns still exist, replace them with ''s, which are more portable if system == 'unknown': @@ -1236,7 +1236,7 @@ # We preserve original uname (more or less) here as _uname: -__uname_cache = None +_uname_cache2 = None def _uname(): @@ -1252,11 +1252,11 @@ Jython-note: _uname resembles CPython behavior for debugging purposes etc. """ - global __uname_cache + global _uname_cache2 no_os_uname = 0 - if __uname_cache is not None: - return __uname_cache + if _uname_cache2 is not None: + return _uname_cache2 processor = '' @@ -1375,8 +1375,8 @@ if system == 'Windows' and processor == '': processor = _java_getenv('PROCESSOR_IDENTIFIER',processor) - __uname_cache = system,node,release,version,machine,processor - return __uname_cache + _uname_cache2 = system,node,release,version,machine,processor + return _uname_cache2 ### Direct interfaces to some of the uname() return values -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Wed Aug 24 17:40:35 2016 From: jython-checkins at python.org (jim.baker) Date: Wed, 24 Aug 2016 21:40:35 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Support_SSL_handshaking_for?= =?utf-8?q?_IPv6_peers=2E_Fixes_=232461?= Message-ID: <20160824214030.17993.35481.4A307A2A@psf.io> https://hg.python.org/jython/rev/3881da5b10a8 changeset: 7933:3881da5b10a8 user: Tom Alexander date: Wed Aug 24 17:40:27 2016 -0400 summary: Support SSL handshaking for IPv6 peers. Fixes #2461 files: Lib/ssl.py | 6 ++---- 1 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/ssl.py b/Lib/ssl.py --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -1073,10 +1073,8 @@ self._key_managers.getKeyManagers(), trust_managers, None) - if hostname is not None: - engine = context.createSSLEngine(hostname, addr[1]) - else: - engine = context.createSSLEngine(*addr) + # addr could be ipv6, only extract relevant parts + engine = context.createSSLEngine((hostname or addr[0]), addr[1]) # apparently this can be used to enforce hostname verification if hostname is not None and self._check_hostname: -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Wed Aug 24 18:58:15 2016 From: jython-checkins at python.org (jim.baker) Date: Wed, 24 Aug 2016 22:58:15 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_commit-subprocess=2Etxt?= Message-ID: <20160824225815.26941.81436.4B473C02@psf.io> https://hg.python.org/jython/rev/99026396b6c2 changeset: 7934:99026396b6c2 user: Jim Baker date: Wed Aug 24 18:58:10 2016 -0400 summary: commit-subprocess.txt files: ACKNOWLEDGMENTS | 1 + Lib/subprocess.py | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ACKNOWLEDGMENTS b/ACKNOWLEDGMENTS --- a/ACKNOWLEDGMENTS +++ b/ACKNOWLEDGMENTS @@ -175,6 +175,7 @@ Jaime Saiz Nick Bailey Doug Clayton + Carl Wicklow Local Variables: mode: indented-text diff --git a/Lib/subprocess.py b/Lib/subprocess.py --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -1426,13 +1426,13 @@ attribute.""" if self.returncode is None: self.returncode = self._process.waitFor() - for coupler in (self._stdout_thread, self._stderr_thread): - if coupler: - coupler.join() - if self._stdin_thread: - # The stdin thread may be blocked forever, forcibly - # stop it - self._stdin_thread.interrupt() + for coupler in (self._stdout_thread, self._stderr_thread): + if coupler: + coupler.join() + if self._stdin_thread: + # The stdin thread may be blocked forever, forcibly + # stop it + self._stdin_thread.interrupt() return self.returncode def terminate(self): -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Wed Aug 24 19:05:35 2016 From: jython-checkins at python.org (jim.baker) Date: Wed, 24 Aug 2016 23:05:35 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Always_join_on_subprocess_c?= =?utf-8?q?oupler_threads=2E_Fixes_=232488?= Message-ID: <20160824230535.17993.98135.4FABA72E@psf.io> https://hg.python.org/jython/rev/35fa19ca1859 changeset: 7935:35fa19ca1859 user: Jim Baker date: Wed Aug 24 19:05:22 2016 -0400 summary: Always join on subprocess coupler threads. Fixes #2488 This join ensures that stdout, stderr is properly flushed even if the subprocess has now exited. Fixes a very old bug that has been in Jython since the introduction of subprocess support in 2.5. Thanks to Carl Wicklow for identifying this indentation bug. files: NEWS | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diff --git a/NEWS b/NEWS --- a/NEWS +++ b/NEWS @@ -4,6 +4,7 @@ Jython 2.7.1rc Bugs fixed + - [ 2488 ] Always join on subprocess coupler threads - [ 2480 ] Repeating from import results in reload - [ 2472 ] Importing simplejson fails with: 'NoneType' object has no attribute 'encode_basestring_ascii' -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:18 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:18 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Zero-length_PyBuffer_should?= =?utf-8?q?_be_able_to_claim_it_is_writable=2E?= Message-ID: <20160827131218.27279.99518.B90F5A9D@psf.io> https://hg.python.org/jython/rev/cdc3278e9cc6 changeset: 7938:cdc3278e9cc6 user: Jeff Allen date: Tue May 31 17:08:21 2016 +0100 summary: Zero-length PyBuffer should be able to claim it is writable. It is less surprising if a zero length slice from a writable object is consistent with the object. Every index is out of bounds, but a bulk write of zero bytes ought to be acceptable. files: src/org/python/core/buffer/ZeroByteBuffer.java | 8 ++++++-- 1 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/org/python/core/buffer/ZeroByteBuffer.java b/src/org/python/core/buffer/ZeroByteBuffer.java --- a/src/org/python/core/buffer/ZeroByteBuffer.java +++ b/src/org/python/core/buffer/ZeroByteBuffer.java @@ -91,7 +91,9 @@ @Override public void copyFrom(byte[] src, int srcPos, int destIndex, int length) throws IndexOutOfBoundsException, PyException { - if (length > 0) { + if (this.isReadonly()) { + throw notWritable(); + } else if (length > 0) { throw new IndexOutOfBoundsException(); } } @@ -102,7 +104,9 @@ */ @Override public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { - if (src.getLen() > 0) { + if (this.isReadonly()) { + throw notWritable(); + } else if (src.getLen() > 0) { throw new IndexOutOfBoundsException(); } } -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:18 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:18 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Add_to_PyBuffer_API_a_repla?= =?utf-8?q?cement_for_Pointer_using_java=2Enio=2EByteBuffer=2E?= Message-ID: <20160827131218.113453.57803.C1792994@psf.io> https://hg.python.org/jython/rev/00f53f67cf47 changeset: 7936:00f53f67cf47 parent: 7918:87534ec6252a user: Jeff Allen date: Sat May 07 13:25:44 2016 +0100 summary: Add to PyBuffer API a replacement for Pointer using java.nio.ByteBuffer. Pointer is deprecated, and the API uses java.nio.ByteBuffer instead. Unit tests are created from the Pointer equivalents. Storage is still implemented as byte[] pending further change. files: src/org/python/core/PyBUF.java | 10 +- src/org/python/core/PyBuffer.java | 43 +- src/org/python/core/buffer/BaseBuffer.java | 29 +- src/org/python/core/buffer/SimpleBuffer.java | 12 +- src/org/python/core/buffer/SimpleStringBuffer.java | 15 +- src/org/python/core/buffer/Strided1DBuffer.java | 2 + src/org/python/core/buffer/ZeroByteBuffer.java | 1 + tests/java/org/python/core/PyBufferTest.java | 240 ++++++++- 8 files changed, 291 insertions(+), 61 deletions(-) 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 @@ -122,7 +122,7 @@ /** * A constant used by the consumer in its call to {@link BufferProtocol#getBuffer(int)} to * specify that it requires {@link PyBuffer#getFormat()} to return a String - * indicating the type of the unit. This exists for compatibility with CPython, as Jython as the + * indicating the type of the unit. This exists for compatibility with CPython, as in Jython the * format is always provided by getFormat(). */ static final int FORMAT = 0x0004; @@ -203,13 +203,14 @@ * 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()}. + * {@link PyBuffer#getNIOByteBuffer()} or using {@link PyBuffer#Pointer()}. */ static final int FULL = INDIRECT | WRITABLE | FORMAT; /** * 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()}. + * only, without ever calling {@link PyBuffer#getNIOByteBuffer()} or using + * {@link PyBuffer#Pointer()}. */ static final int FULL_RO = INDIRECT | FORMAT; @@ -221,7 +222,8 @@ * through the purely abstract part of the API). getBuffer will raise an exception * if the exporter cannot expose its storage as Java array. */ - static final int AS_ARRAY = 0x10000000; + // XXX Pending: @Deprecated + static final int AS_ARRAY = 0x10000000; /* Constants for readability, not standard for CPython */ 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 @@ -244,8 +244,11 @@ /** * Obtain a {@link java.nio.ByteBuffer} giving access to the bytes that hold the data being - * exported to the consumer. For a one-dimensional contiguous buffer, assuming the following - * client code where obj has type BufferProtocol: + * exported to the consumer. The position of the buffer is at the item with zero index, the + * limit of the buffer is one beyond the largest valid index, and the mark is undefined. + *

+ * For a one-dimensional contiguous buffer, assuming the following client code where + * obj has type BufferProtocol: * *

      * PyBuffer a = obj.getBuffer(PyBUF.SIMPLE);
@@ -253,11 +256,13 @@
      * ByteBuffer bb = a.getNIOBuffer();
      * 
* - * the item with index bb.pos()+k is in the buffer bb at positions + * the item with index k is in bb at positions * bb.pos()+k*itemsize to bb.pos()+(k+1)*itemsize - 1 inclusive. And * if itemsize==1, the item is simply the byte at position bb.pos()+k. * The buffer limit is set to the first byte beyond the valid data. A block read or write will - * therefore access the contents sequentially. + * therefore access the contents sequentially. In a one-dimensional contiguous buffer (only) it + * is safe to rely on bb.remaining() to obtain the number of bytes representing the + * object state. *

* If the buffer is multidimensional or non-contiguous (strided), the buffer position is still * the (first byte of) the item at index [0] or [0,...,0], and the @@ -265,11 +270,35 @@ * using the shape, strides and maybe suboffsets provided * by the API. * - * @return a ByteBuffer equivalent to the exported data contents. + * @return a ByteBuffer onto the exported data contents. */ ByteBuffer getNIOByteBuffer(); - // Direct access to actual storage + /** + * Obtain a {@link java.nio.ByteBuffer} giving access to the bytes that hold the data being + * exported to the consumer, and positioned at a particular index. + *

+ * Essentially this saves the client from computing the byte offset of a particular index. The + * client is free to navigate the underlying byte data through the ByteBuffer. + * + * @param index in the buffer to position the pointer + * @return a ByteBuffer onto the exported data contents, positioned at the indexed item. + */ + ByteBuffer getNIOByteBuffer(int index); + + /** + * Obtain a {@link java.nio.ByteBuffer} giving access to the bytes that hold the data being + * exported to the consumer, and positioned at a particular multi-dimensional index. + *

+ * Essentially this saves the client from computing the byte offset of a particular index. The + * client is free to navigate the underlying byte data through the ByteBuffer. + * + * @param indices multidimensional index at which to position the pointer + * @return a ByteBuffer onto the exported data contents, positioned at the indexed item. + */ + ByteBuffer getNIOByteBuffer(int... indices); + + // Direct access to actual storage (deprecated) // /** @@ -279,6 +308,7 @@ * * @return true if array access is supported, false if it is not. */ + // XXX Pending: @Deprecated boolean hasArray(); /** @@ -288,6 +318,7 @@ * this array, and in others not. See {@link PyBuffer#isReadonly()}. It is used by the Jython * buffer API roughly where the CPython buffer API uses a C (char *) pointer. */ + @Deprecated public static class Pointer { /** Reference to the array holding the bytes. */ 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 @@ -65,6 +65,7 @@ * (contiguous cases) or the index in storage that should be treated as the item * with index zero (retrieved say by {@link #byteAt(int)}). */ + // XXX Pending change of implementation to ByteBuffer protected byte[] storage; /** @@ -600,23 +601,43 @@ @Override public ByteBuffer getNIOByteBuffer() { - // Determine the limit of the buffer just beyond the last item. - int length = calcGreatestIndex() + getItemsize() - index0; - ByteBuffer b = ByteBuffer.wrap(storage, index0, length); + // The buffer spans the whole storage, which may include data not in the view + ByteBuffer b = ByteBuffer.wrap(storage); + b.limit(calcGreatestIndex() + getItemsize()).position(index0); // Return as read-only if it is. return isReadonly() ? b.asReadOnlyBuffer() : b; } @Override + public ByteBuffer getNIOByteBuffer(int index) { + // The buffer spans the whole storage but is positioned at index + ByteBuffer b = getNIOByteBuffer(); + b.position(calcIndex(index)); + return b; + } + + @Override + public ByteBuffer getNIOByteBuffer(int... indices) { + // The buffer spans the whole storage but is positioned at indices[] + ByteBuffer b = getNIOByteBuffer(); + b.position(calcIndex(indices)); + // Return as read-only if it is. + return b; + } + + @SuppressWarnings("deprecation") + @Override public Pointer getBuf() { return new Pointer(storage, index0); } + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int index) throws IndexOutOfBoundsException { return new Pointer(storage, calcIndex(index)); } + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int... indices) throws IndexOutOfBoundsException { return new Pointer(storage, calcIndex(indices)); @@ -664,7 +685,7 @@ /** * Some PyBuffers, those created by slicing a PyBuffer are related to * a root PyBuffer. During creation of such a slice, we need to supply a value for - * this root. If the present object is not itself a slice, this is root is the object itself; if + * this root. If the present object is not itself a slice, this root is the object itself; if * the buffer is already a slice, it is the root it was given at creation time. Often this is * the only difference between a slice-view and a directly-exported buffer. Override this method * in slices to return the root buffer of the slice. 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 @@ -50,8 +50,7 @@ * @throws ArrayIndexOutOfBoundsException if index0 and size are * inconsistent with storage.length */ - // XXX: "for sub-class use" = should be protected? - public SimpleBuffer(byte[] storage, int index0, int size) throws PyException, + protected SimpleBuffer(byte[] storage, int index0, int size) throws PyException, ArrayIndexOutOfBoundsException { this(); this.storage = storage; // Exported data @@ -230,18 +229,13 @@ } } - @Override - public ByteBuffer getNIOByteBuffer() { - // Simplify for one-dimensional contiguous bytes - ByteBuffer b = ByteBuffer.wrap(storage, index0, shape[0]); - return isReadonly() ? b.asReadOnlyBuffer() : b; - } - + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int index) throws IndexOutOfBoundsException { return new Pointer(storage, index0 + index); } + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int... indices) throws IndexOutOfBoundsException { checkDimension(indices.length); diff --git a/src/org/python/core/buffer/SimpleStringBuffer.java b/src/org/python/core/buffer/SimpleStringBuffer.java --- a/src/org/python/core/buffer/SimpleStringBuffer.java +++ b/src/org/python/core/buffer/SimpleStringBuffer.java @@ -8,11 +8,11 @@ /** * Buffer API that appears to be a one-dimensional array of one-byte items providing read-only API, * but which is actually backed by a Java String. Some of the buffer API absolutely needs access to - * the data as a byte array (those parts that involve a PyBuffer.Pointer result), and - * therefore this class must create a byte array from the String for them. However, it defers - * creation of a byte array until that part of the API is actually used. Where possible, this class - * overrides those methods in SimpleBuffer that would otherwise access the byte array attribute to - * use the String instead. + * the data as a byte array (those parts that involve a {@link java.nio.ByteBuffer} or + * {@link PyBuffer.Pointer} result), and therefore this class must create a byte array from the + * String for them. However, it defers creation of a byte array until that part of the API is + * actually used. Where possible, this class overrides those methods in SimpleBuffer that would + * otherwise access the byte array attribute to use the String instead. */ public class SimpleStringBuffer extends SimpleBuffer { @@ -124,7 +124,7 @@ public ByteBuffer getNIOByteBuffer() { // Force creation of the actual byte array from the String. ensureHaveBytes(); - return super.getNIOByteBuffer().asReadOnlyBuffer(); + return super.getNIOByteBuffer(); } /** @@ -142,6 +142,7 @@ *

* This method creates an actual byte array from the underlying String if none yet exists. */ + @SuppressWarnings("deprecation") @Override public Pointer getBuf() { ensureHaveBytes(); @@ -153,6 +154,7 @@ *

* This method creates an actual byte array from the underlying String if none yet exists. */ + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int index) { ensureHaveBytes(); @@ -164,6 +166,7 @@ *

* This method creates an actual byte array from the underlying String if none yet exists. */ + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int... indices) { ensureHaveBytes(); 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 @@ -224,11 +224,13 @@ } } + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int index) { return new Pointer(storage, index0 + index * stride); } + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int... indices) { // BaseBuffer implementation can be simplified since if indices.length!=1 we error. diff --git a/src/org/python/core/buffer/ZeroByteBuffer.java b/src/org/python/core/buffer/ZeroByteBuffer.java --- a/src/org/python/core/buffer/ZeroByteBuffer.java +++ b/src/org/python/core/buffer/ZeroByteBuffer.java @@ -135,6 +135,7 @@ *

* The implementation in ZeroByteBuffer efficiently returns an empty buffer. */ + @SuppressWarnings("deprecation") @Override public Pointer getBuf() { // Has to be new because the client is allowed to manipulate the contents. 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 @@ -3,6 +3,7 @@ import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedList; @@ -959,10 +960,108 @@ byte[] result = bytesFromByteAt(sliceView); assertBytesEqual(" testGetBufferSliceWithStride failure: ", expected, result); } + /** + * Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer()}. + */ + public void testGetNIOByteBuffer() { + for (BufferTestPair test : buffersToRead) { + if (verbosity > 0) { + System.out.println("getNIOByteBuffer: " + test); + } + int stride = test.strides[0]; + + 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); + + ByteBuffer bp = view.getNIOByteBuffer(); + 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 + ByteBuffer bp = view.getNIOByteBuffer(); + assertBytesEqual("buffer does not match reference", test.material.bytes, bp, stride); + } + + } + } + + /** + * Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer(int)}. + */ + public void testGetNIOByteBuffer_int() { + for (BufferTestPair test : buffersToRead) { + if (verbosity > 0) { + System.out.println("getNIOByteBuffer(int): " + test); + } + PyBuffer view = test.view; + int n = test.material.length, itemsize = view.getItemsize(); + byte[] exp = new byte[itemsize], bytes = test.material.bytes; + + for (int i = 0; i < n; i++) { + // Expected result is one item (allow for itemsize) + int p = i * itemsize; + for (int j = 0; j < itemsize; j++) { + exp[j] = bytes[p + j]; + } + + // Get buffer and check for correct data at bb.position() + ByteBuffer bb = view.getNIOByteBuffer(i); + assertBytesEqual("getNIOByteBuffer(int) value", exp, bb); + } + } + } + + /** + * Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer(int[])}. + */ + public void testGetNIOByteBuffer_intArray() { + int[] index = new int[1]; + for (BufferTestPair test : buffersToRead) { + if (verbosity > 0) { + System.out.println("getNIOByteBuffer(int[]): " + test); + } + PyBuffer view = test.view; + int n = test.material.length, itemsize = view.getItemsize(); + byte[] exp = new byte[itemsize], bytes = test.material.bytes; + + for (int i = 0; i < n; i++) { + // Expected result is one item (allow for itemsize) + int p = i * itemsize; + for (int j = 0; j < itemsize; j++) { + exp[j] = bytes[p + j]; + } + + // Get buffer and check for correct data at bb.position() + index[0] = i; + ByteBuffer bb = view.getNIOByteBuffer(index); + assertBytesEqual("getNIOByteBuffer(int[]) value", exp, bb); + } + + // Check 2D index throws + try { + view.getNIOByteBuffer(0, 0); + fail("Use of 2D index did not raise exception"); + } catch (PyException pye) { + // Expect BufferError + assertEquals(Py.BufferError, pye.type); + } + } + } + + /** * Test method for {@link org.python.core.PyBuffer#getBuf()}. */ + @SuppressWarnings("deprecation") public void testGetBuf() { for (BufferTestPair test : buffersToRead) { if (verbosity > 0) { @@ -980,6 +1079,7 @@ 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); @@ -995,6 +1095,7 @@ /** * Test method for {@link org.python.core.PyBuffer#getPointer(int)}. */ + @SuppressWarnings("deprecation") public void testGetPointer() { for (BufferTestPair test : buffersToRead) { if (verbosity > 0) { @@ -1021,6 +1122,7 @@ /** * Test method for {@link org.python.core.PyBuffer#getPointer(int[])}. */ + @SuppressWarnings("deprecation") public void testGetPointerNdim() { int[] index = new int[1]; for (BufferTestPair test : buffersToRead) { @@ -1458,49 +1560,121 @@ } /** - * Customised assert method comparing a buffer pointer to a byte array, usually the one from - * ByteMaterial. + * Custom assert method comparing the bytes at a {@link PyBuffer.Pointer} to those in a byte + * array, when that Pointer is obtained from a contiguous PyBuffer. + * Let bp[i] denote bp.storage[bp.offset+i], by analogy with a C + * pointer. It is required that bp[k] == expected[k], for every index in + * expected. If not, a fail() is declared. * * @param message to issue on failure * @param expected expected byte array * @param bp result to test */ + @SuppressWarnings("deprecation") static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp) { assertBytesEqual(message, expected, bp, 1); } /** - * Customised assert method comparing a buffer pointer to a byte array, usually the one from - * ByteMaterial. + * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte + * array, when that ByteBuffer is obtained from a contiguous PyBuffer. + * Let bb[i] denote bb.get(bb.position()+i), by analogy with a C + * pointer. It is required that bb[k] == expected[k], for every index + * k in expected. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param bb result to test + */ + static void assertBytesEqual(String message, byte[] expected, ByteBuffer bb) { + // Use the position-advancing buffer get() + byte[] actual = new byte[expected.length]; + assertEquals( message + " (size in buffer)", expected.length, bb.remaining()); + bb.get(actual); + assertBytesEqual(message, expected, actual); + } + + /** + * Custom assert method comparing the bytes at a {@link PyBuffer.Pointer} to those in a byte + * array, when that Pointer is obtained from a striding PyBuffer. Let + * bp[i] denote bp.storage[bp.offset+i], by analogy with a C pointer. + * It is required that bp[k*stride] == expected[k], for every index k + * in expected. If not, a fail() is declared. * * @param message to issue on failure * @param expected expected byte array * @param bp result to test - * @param stride in the storage array + * @param stride in the bp.storage array */ + @SuppressWarnings("deprecation") static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp, int stride) { assertBytesEqual(message, expected, 0, expected.length, bp.storage, bp.offset, stride); } /** - * Customised assert method comparing a buffer pointer to a byte array, usually the one from - * ByteMaterial. + * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte + * array, when that ByteBuffer is obtained from a striding PyBuffer. + * Let bb[i] denote bb.get(bb.position()+i), by analogy with a C + * pointer. It is required that bb[k*stride] == expected[k], for every index + * k in expected. If not, a fail() is declared. * * @param message to issue on failure * @param expected expected byte array - * @param expectedStart where to start the comparison in expected + * @param bb result to test + * @param stride in the buffer bb + */ + static void assertBytesEqual(String message, byte[] expected, ByteBuffer bb, int stride) { + assertBytesEqual(message, expected, 0, expected.length, bb, stride); + } + + /** + * Custom assert method comparing the bytes at a {@link PyBuffer.Pointer} to those in a byte + * array, when that Pointer is obtained from a striding PyBuffer. Let + * bp[i] denote bp.storage[bp.offset+i], by analogy with a C pointer. + * It is required that bp[k*stride] == expected[expectedStart+k], for + * k=0 to n-1. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param expectedStart where to start the comparison in expected * @param n number of bytes to test - * @param bb result to test - * @param stride in the storage array + * @param bp result to test + * @param stride in the bp.storage array */ + @SuppressWarnings("deprecation") static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, PyBuffer.Pointer bp, int stride) { assertBytesEqual(message, expected, expectedStart, n, bp.storage, bp.offset, stride); } /** - * Customised assert method comparing a byte arrays: values in the actual value must match all - * those in expected[], and they must be the same length. + * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte + * array, when that ByteBuffer is obtained from a striding PyBuffer. + * Let bb[i] denote bb.get(bb.position()+i), by analogy with a C + * pointer. It is required that bb[k*stride] == expected[expectedStart+k], for + * k=0 to n-1. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param expectedStart where to start the comparison in expected + * @param n number of bytes to test + * @param bb result to test + * @param stride in the buffer bb + */ + static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, + ByteBuffer bb, int stride) { + // Note that this approach leaves the buffer position unmodified + int p = bb.position(); + byte[] actual = new byte[n]; + for (int k = 0; k < n; k++, p += stride) { + actual[k] = bb.get(p); + } + assertBytesEqual(message, expected, expectedStart, n, actual, 0); + } + + /** + * Custom assert method comparing byte arrays: values in actual[] must match all + * those in expected[], and they must be the same length. * * @param message to issue on failure * @param expected expected byte array @@ -1512,28 +1686,30 @@ } /** - * 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. + * Custom assert method comparing byte arrays. It is required that + * actual[k] == expected[k], for k=0 to expected.length-1 + * . If not, a fail() is declared. * * @param message to issue on failure * @param expected expected byte array * @param actual result to test - * @param actualStart where to start the comparison in actual + * @param actualStart where to start the comparison in actual */ static void assertBytesEqual(String message, byte[] expected, byte[] actual, int actualStart) { assertBytesEqual(message, expected, 0, expected.length, actual, actualStart, 1); } /** - * Customised assert method comparing byte arrays: values starting at actual[actualStart] must - * those starting at expected[expectedStart], for a distance of n bytes. + * Custom assert method comparing byte arrays. It is required that + * actual[actualStart+k] == expected[expectedStart+k], for k=0 to + * n-1. If not, a fail() is declared. * * @param message to issue on failure * @param expected expected byte array - * @param expectedStart where to start the comparison in expected + * @param expectedStart where to start the comparison in expected * @param n number of bytes to test * @param actual result to test - * @param actualStart where to start the comparison in actual + * @param actualStart where to start the comparison in actual */ static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, byte[] actual, int actualStart) { @@ -1541,16 +1717,17 @@ } /** - * Customised assert method comparing byte arrays: values starting at actual[actualStart] must - * those starting at expected[expectedStart], for a distance of n bytes. + * Custom assert method comparing byte arrays. It is required that + * actual[actualStart+k*stride] == expected[expectedStart+k], for + * k=0 to n-1. If not, a fail() is declared. * * @param message to issue on failure * @param expected expected byte array - * @param expectedStart where to start the comparison in expected + * @param expectedStart where to start the comparison in expected * @param n number of bytes to test * @param actual result to test - * @param actualStart where to start the comparison in actual - * @param stride spacing of bytes in actual array + * @param actualStart where to start the comparison in actual + * @param stride spacing of bytes in actual array */ static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, byte[] actual, int actualStart, int stride) { @@ -1579,13 +1756,10 @@ // 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))); + byte[] a = Arrays.copyOfRange(actual, actualStart, actualStart + n); + byte[] e = Arrays.copyOfRange(expected, expectedStart, expectedStart + n); + System.out.println(" expected:" + Arrays.toString(e)); + System.out.println(" actual:" + Arrays.toString(a)); System.out.println(" _actual_:" + Arrays.toString(actual)); fail(message + " (byte at " + j + ")"); } @@ -1771,7 +1945,9 @@ @Override public String toString() { - int offset = view.getBuf().offset; + int offset0 = view.getBuf().offset; // XXX -> view.getNIOByteBuffer().position() ? + int offset = view.getNIOByteBuffer().position(); + assertEquals(offset0, offset); String offsetSpec = offset > 0 ? "[0@(" + offset + "):" : "[:"; int stride = strides[0]; String sliceSpec = offsetSpec + shape[0] + (stride != 1 ? "*(" + stride + ")]" : "]"); -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:18 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:18 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Refactor_PyBufferTest_into_?= =?utf-8?q?a_test_and_its_support=2C_and_use_JUnit4=2E?= Message-ID: <20160827131218.2296.20751.974B93E8@psf.io> https://hg.python.org/jython/rev/fab295b87737 changeset: 7937:fab295b87737 user: Jeff Allen date: Tue May 31 16:50:36 2016 +0100 summary: Refactor PyBufferTest into a test and its support, and use JUnit4. JUnit4 parameterisation lets us break down test cases into smaller units for debugging. Many supporting functions are abstracted, with a view to re-use when testing a NIO-based buffer API. Two latent bugs are detected, but (for clarity) not fixed until next commit. files: tests/java/org/python/core/ByteBufferTestSupport.java | 585 ++ tests/java/org/python/core/PyBufferTest.java | 2204 +++------ tests/java/org/python/core/PyBufferTestSupport.java | 543 ++ 3 files changed, 1865 insertions(+), 1467 deletions(-) diff --git a/tests/java/org/python/core/ByteBufferTestSupport.java b/tests/java/org/python/core/ByteBufferTestSupport.java new file mode 100644 --- /dev/null +++ b/tests/java/org/python/core/ByteBufferTestSupport.java @@ -0,0 +1,585 @@ +package org.python.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * Common apparatus for tests involving byte[] and java.nio.ByteBuffer + * material, in particular the tests of {@link PyBuffer} implementations. A test case that extends + * this class will be equipped with additional assertion methods and a class to represent + * byte[] test material in several forms. + */ +public class ByteBufferTestSupport { + + /** + * Class to hold test material representing the same sequence of values 0..255 in several + * different ways. + */ + protected static class ByteMaterial { + + /** Length in bytes (length of every array in this material). */ + final int length; + /** The byte values. */ + byte[] bytes; + /** The byte values individually as ints. */ + int[] ints; + /** The byte values treated as characters (unicode < 256). */ + String string; + + /** Construct from int array. */ + public ByteMaterial(int[] a) { + ints = a.clone(); + length = replicate(); + } + + /** Construct from String. */ + public ByteMaterial(String s) { + ints = new int[s.length()]; + for (int i = 0; i < ints.length; i++) { + ints[i] = 0xff & s.charAt(i); + } + length = replicate(); + } + + /** Construct from byte array. */ + public ByteMaterial(byte[] b) { + ints = new int[b.length]; + for (int i = 0; i < ints.length; i++) { + ints[i] = 0xff & b[i]; + } + length = replicate(); + } + + /** Construct from pattern on values (used modulo 256). */ + public ByteMaterial(int start, int count, int inc) { + ints = new int[count]; + int x = start; + for (int i = 0; i < count; i++) { + ints[i] = x; + x = (x + inc) & 0xff; + } + length = replicate(); + } + + /** + * Once the integer value array {@link #ints} has been initialised, fill the others from it. + * + * @return length of (all) arrays in units + */ + private int replicate() { + int n = ints.length; + bytes = new byte[n]; + StringBuilder sb = new StringBuilder(n); + + for (int i = 0; i < n; i++) { + int x = ints[i]; + bytes[i] = (byte)x; + sb.appendCodePoint(x); + } + string = sb.toString(); + return n; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(100); + sb.append("byte[").append(length).append("]={ "); + for (int i = 0; i < length; i++) { + if (i > 0) { + sb.append(", "); + } + if (i >= 5) { + sb.append(" ..."); + break; + } else { + sb.append(ints[i]); + } + } + sb.append(" }"); + return sb.toString(); + } + + /** + * @return a copy of the bytes array (that the client is allowed to modify) + */ + byte[] getBytes() { + return bytes.clone(); + } + + /** + * @return a buffer on a copy of the bytes array (that the client is allowed to modify) + */ + ByteBuffer getBuffer() { + return ByteBuffer.wrap(getBytes()); + } + + /** + * Create material equivalent to a slice of this material. This may act as a reference + * result for testing slice operations. + * + * @param start first byte-index to include + * @param length number of items + * @param stride between byte-indices + * @return ByteMaterial in which the arrays are a slice of this one + */ + ByteMaterial slice(int start, int length, int stride) { + return new ByteMaterial(sliceBytes(bytes, 1, start, length, stride)); + } + + /** + * Create material equivalent to a slice of this material. This may act as a reference + * result for testing slice operations. + * + * @param start first byte-index to include + * @param itemsize number of consecutive bytes treated as one item + * @param length number of items + * @param stride between byte-indices + * @return ByteMaterial in which the arrays are a slice of this one + */ + ByteMaterial slice(int itemsize, int start, int length, int stride) { + return new ByteMaterial(sliceBytes(bytes, itemsize, start, length, stride)); + } + } + + /** + * Create a byte array that is a strided copy of the one passed in. The specifications are + * assumed correct for the size of that array. + * + * @param b source array + * @param start first index to include + * @param length number of indices + * @param stride between indices + * @return slice of b + */ + protected static byte[] sliceBytes(byte[] b, int start, int length, int stride) { + return sliceBytes(b, 1, start, length, stride); + } + + /** + * Create a multi-byte item array that is a strided copy of the one passed in. The + * specifications are assumed correct for the size of that array. + * + * @param b source array + * @param itemsize number of consecutive bytes treated as one item + * @param start first byte-index to include + * @param length number of indices to visit (items to copy) + * @param stride between byte-indices + * @return slice of b + */ + protected static byte[] sliceBytes(byte[] b, int itemsize, int start, int length, int stride) { + byte[] a = new byte[length]; + for (int i = 0, j = start; i < length; i++, j += stride) { + for (int k = 0; k < itemsize; k++) { + a[i + k] = b[j + k]; + } + } + return a; + } + + /** + * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte + * array, when that ByteBuffer is obtained from a contiguous PyBuffer. + * Let bb[i] denote bb.get(bb.position()+i), by analogy with a C + * pointer. It is required that bb[k] == expected[k], for every index + * k in expected. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param bb result to test + */ + static void assertBytesEqual(String message, byte[] expected, ByteBuffer bb) { + // Use the position-advancing buffer get() + byte[] actual = new byte[expected.length]; + bb.get(actual); + assertBytesEqual(message, expected, actual); + } + + /** + * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte + * array, when that ByteBuffer is obtained from a striding PyBuffer. + * Let bb[i] denote bb.get(bb.position()+i), by analogy with a C + * pointer. It is required that bb[k*stride] == expected[k], for every index + * k in expected. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param bb result to test + * @param stride in the buffer bb + */ + static void assertBytesEqual(String message, byte[] expected, ByteBuffer bb, int stride) { + assertBytesEqual(message, expected, 0, expected.length, bb, stride); + } + + /** + * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte + * array, when that ByteBuffer is obtained from a striding PyBuffer. + * Let bb[i] denote bb.get(bb.position()+i), by analogy with a C + * pointer. It is required that bb[k*stride] == expected[expectedStart+k], for + * k=0 to n-1. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param expectedStart where to start the comparison in expected + * @param n number of bytes to test + * @param bb result to test + * @param stride in the buffer bb + */ + static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, + ByteBuffer bb, int stride) { + // Note that this approach leaves the buffer position unmodified + int p = bb.position(); + byte[] actual = new byte[n]; + for (int k = 0; k < n; k++, p += stride) { + actual[k] = bb.get(p); + } + assertBytesEqual(message, expected, expectedStart, n, actual, 0); + } + + /** + * Custom assert method comparing byte arrays: values in actual[] must match all + * those in expected[], and they must be the same length. + * + * @param message to issue on failure + * @param expected expected byte array + * @param actual result to test + */ + static void assertBytesEqual(String message, byte[] expected, byte[] actual) { + assertEquals(message + " (array size)", expected.length, actual.length); + assertBytesEqual(message, expected, 0, expected.length, actual, 0, 1); + } + + /** + * Custom assert method comparing byte arrays. It is required that + * actual[k] == expected[k], for k=0 to expected.length-1 + * . If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param actual result to test + * @param actualStart where to start the comparison in actual + */ + static void assertBytesEqual(String message, byte[] expected, byte[] actual, int actualStart) { + assertBytesEqual(message, expected, 0, expected.length, actual, actualStart, 1); + } + + /** + * Custom assert method comparing byte arrays. It is required that + * actual[actualStart+k] == expected[expectedStart+k], for k=0 to + * n-1. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param expectedStart where to start the comparison in expected + * @param n number of bytes to test + * @param actual result to test + * @param actualStart where to start the comparison in actual + */ + protected static void assertBytesEqual(String message, byte[] expected, int expectedStart, + int n, byte[] actual, int actualStart) { + assertBytesEqual(message, expected, expectedStart, n, actual, actualStart, 1); + } + + /** + * Custom assert method comparing byte arrays. It is required that + * actual[actualStart+k*stride] == expected[expectedStart+k], for k=0 + * to n-1. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param expectedStart where to start the comparison in expected + * @param n number of bytes to test + * @param actual result to test + * @param actualStart where to start the comparison in actual + * @param stride spacing of bytes in actual array + */ + static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, + byte[] actual, int actualStart, int stride) { + + if (actualStart < 0) { + fail(message + " (start<0 in result)"); + + } else if (expectedStart < 0) { + fail(message + " (start<0 in expected result): bug in test?"); + + } else if (actualStart + (n - 1) * stride + 1 > actual.length) { + fail(message + " (result too short)"); + + } else if (expectedStart + n > expected.length) { + fail(message + " (expected result too short): bug in test?"); + + } else { + // Should be safe to compare the values + int i = actualStart, j, jLimit = expectedStart + n; + for (j = expectedStart; j < jLimit; j++) { + if (actual[i] != expected[j]) { + break; + } + i += stride; + } + + // If we stopped early, diagnose the problem + if (j < jLimit) { + byte[] a = Arrays.copyOfRange(actual, actualStart, actualStart + n); + byte[] e = Arrays.copyOfRange(expected, expectedStart, expectedStart + n); + System.out.println(" expected:" + Arrays.toString(e)); + System.out.println(" actual:" + Arrays.toString(a)); + System.out.println(" _actual_:" + Arrays.toString(actual)); + fail(message + " (byte at " + j + ")"); + } + } + } + + /** + * Customised assert method comparing a int arrays: values in the actual value starting at + * actual[offset] must match all those in expected[], and there must be enough of them. + * + * @param message to issue on failure + * @param expected expected array + * @param actual result to test + * @param offset where to start the comparison in actual + */ + static void assertIntsEqual(String message, int[] expected, int[] actual, int offset) { + int n = expected.length; + if (offset < 0) { + fail(message + " (offset<0)"); + } else if (offset + n > actual.length) { + fail(message + " (too short)"); + } else { + // Should be safe to compare the values + int i = offset, j; + for (j = 0; j < n; j++) { + if (actual[i++] != expected[j]) { + break; + } + } + if (j < n) { + System.out.println(" expected:" + Arrays.toString(expected)); + System.out.println(" actual:" + Arrays.toString(actual)); + fail(message + " (int at " + j + ")"); + } + } + } + + /** + * Customised assert method comparing a int arrays: int in the actual value must match all those + * in expected[], and there must be the same number of them. + * + * @param message to issue on failure + * @param expected expected array + * @param actual result to test + */ + protected static void assertIntsEqual(String message, int[] expected, int[] actual) { + int n = expected.length; + assertEquals(message, n, actual.length); + // Should be safe to compare the values + int j; + for (j = 0; j < n; j++) { + if (actual[j] != expected[j]) { + break; + } + } + if (j < n) { + System.out.println(" expected:" + Arrays.toString(expected)); + System.out.println(" actual:" + Arrays.toString(actual)); + fail(message + " (int at " + j + ")"); + } + } + + /** + * Method comparing byte arrays after a read (or view creation) operation involving a slice. + *

+ * The invariant asserted must be explained carefully because of its generality. Suppose there + * to be three arrays of bytes a, b and c. Let a represent the state + * of some byte storage of length L before an operation. Let b represent the state + * of the same storage after an operation. Let c be related as follows. + *

+ * c is the result, representing n blocks of u bytes copied from the + * storage, the kth block starting at position s+kp in the storage and at + * t+ku in c. c is of length M≥nu, and we assume + * 0≤s+kp<L. After a read operation, it is required that: + *

    + *
  1. c[t+iu+j] = b[s+ip+j] for 0≤i<n and 0≤j<u, and
  2. + *
  3. a[k] = b[k] for 0≤k<L. + *
+ *

+ * + * @param a storage state before the operation (typically reference data) + * @param b storage state after the operation (typically from object under test) + * @param c bytes read + * @param t index in c of the start byte of item 0 + * @param n number of items + * @param u number of consecutive bytes per item + * @param s index in b of the start byte of item 0 + * @param p the distance in b between the start bytes of successive items + */ + static void checkReadCorrect(byte[] a, byte[] b, byte[] c, int t, int n, int u, int s, int p) { + // Check the b is the same as a + assertEquals("Storage size differs from reference", a.length, b.length); + for (int k = 0; k < b.length; k++) { + if (a[k] != b[k]) { + fail("Stored data changed during read"); + } + } + // Check slice read out + checkEqualInSlice(b, c, t, n, u, s, p); + } + + /** + * Method comparing byte arrays where a change operation has taken place involving a slice. + *

+ * The invariant asserted must be explained carefully because of its generality. Suppose there + * to be three arrays of bytes a, b and c. Let a represent the state + * of some byte storage of length L before an operation. Let b represent the state + * of the same storage after an operation. Let c be related as follows. + *

+ * c is the source, contaning at index t, n blocks of u bytes copied + * to the storage. As before, the kth block starts at position s+kp in the storage + * and at t+ku in c. c is of length M≥t+nu, and we assume + * 0≤s+kp<L. After a write operation, it is required that: + *

    + *
  1. c[t+iu+j] = b[s+ip+j] for 0≤i<n and 0≤j<u, and
  2. + *
  3. a[k] = b[k] for 0≤k<L and k≠s+ip+j for any choice of + * and j where 0≤i<n and 0≤j<u. + *
+ * Note that the first of these is the same as for a read and the second requires equality + * "everywhere else". + * + * @param a storage state before the operation (typically reference data) + * @param b storage state after the operation (typically from object under test) + * @param c bytes written + * @param t index in c of the start byte of item 0 + * @param n number of items + * @param u number of consecutive bytes per item + * @param s index in b of the start byte of item 0 + * @param p the distance in b between the start bytes of successive items + */ + static void checkWriteCorrect(byte[] a, byte[] b, byte[] c, int t, int n, int u, int s, int p) { + assertEquals("Stored size has changed", a.length, b.length); + checkEqualInSlice(b, c, t, n, u, s, p); + checkUnchangedElsewhere(a, b, n, u, s, p); + } + + /** + * Method comparing bytes in a slice pattern of one byte array to bytes taken consecutively in + * another array. This is needed in testing when bytes have been copied into or out of an array + * slice. + *

+ * Let b represent the state of the byte storage of length L after the copy + * operation (the sliced array). Let c be a source or destination array, a section of + * which at index t represents n blocks of u bytes copied to or from the + * storage. c is of length at least t+nu. The kth block starts at position + * s+kp in the storage b and at t+ku in c, and we assume + * 0≤s+kp<L. After a write operation, it is required that: c[t+iu+j] = + * b[s+ip+j] for 0≤i<n and 0≤j<u. + * + * + * @param b storage state after the operation (typically from object under test) + * @param c bytes written + * @param t index in c of the start byte of item 0 + * @param n number of items + * @param u number of consecutive bytes per item + * @param s index in b of the start byte of item 0 + * @param p the distance in b between the start bytes of successive items + */ + static void checkEqualInSlice(byte[] b, byte[] c, int t, int n, int u, int s, int p) { + // Check correct use of the test + checkSliceArgs(b, c, t, n, u, s, p); + + // Check the data in copied units (and p-u bytes following) + for (int i = 0; i < n; i++) { + int ci = t + i * u, bi = s + i * p; + for (int j = 0; j < u; j++, bi++, ci++) { + // Compare corresponding bytes of this unit in c and b + if (c[ci] != b[bi]) { + fail(String.format("contiguous data at %d not equal to buffer at %d", ci, bi)); + } + } + } + } + + /** + * Method comparing the before and after state of the parts of a byte array that should be + * untouched where a change operation has taken place involving a slice. + *

+ * Let a represent the state of some byte storage of length L before an operation. + * Let b represent the state of the same storage after an operation. After a write + * operation, it is required that: a[k] = b[k] for 0≤k<L and + * k≠s+ip+j for any choice of and j where 0≤i<n and + * 0≤j<u. + *

+ * Note that requires equality "everywhere else" than in the slice defined by n units of + * size u starting at s. + * + * @param a storage state before the operation (typically reference data) + * @param b storage state after the operation (typically from object under test) + * @param n number of items + * @param u number of consecutive bytes per item + * @param s index in b of the start byte of item 0 + * @param p the distance in b between the start bytes of successive items + */ + static void checkUnchangedElsewhere(byte[] a, byte[] b, int n, int u, int s, int p) { + // Check correct use of the test + assertEquals("Stored size has changed", a.length, b.length); + assertFalse("Unit size exceeds spacing", u > p && u + p > 0); + String bufferChangedAt = "buffer changed at %d (outside slice)"; + + int absp, low, high; + + if (n < 1) { + // No elements: check whole array. + absp = low = high = 0; + } else if (p >= 0) { + // Stride is forwards in the range (easy case) + absp = p; + // Lowest byte index in the data is byte 0 of first unit in slice + low = s; + // One after highest byte index is just beyond last byte of last unit in slice + high = s + (n - 1) * p + u; + } else { + // p<0: stride is backwards in the range (delicate case) + absp = -p; + // Lowest byte index in the data is byte 0 of last unit in slice + low = s + (n - 1) * p; + // One after highest byte index is just beyond last byte of first unit in slice + high = s + u; + } + + // Visit each copied unit (from low to high byte index) except the highest. + for (int i = 0; i < n - 1; i++) { + int bi = low + i * absp + u; + // Check there was no change to the absp-u bytes following unit in b + for (int j = u; j < absp; j++, bi++) { + if (b[bi] != a[bi]) { + fail(String.format(bufferChangedAt, bi)); + } + } + } + + // Check that b[0:low] is unchanged + for (int k = 0; k < low; k++) { + if (b[k] != a[k]) { + fail(String.format(bufferChangedAt, k)); + } + } + + // Check that [high:] is unchanged + for (int k = Math.max(high, 0); k < b.length; k++) { + if (b[k] != a[k]) { + fail(String.format(bufferChangedAt, k)); + } + } + } + + /** Common code for checkReadCorrect and checkWriteCorrect. */ + private static void checkSliceArgs(byte[] b, byte[] c, int t, int n, int u, int s, int p) { + // Check correct use of the test + assertFalse("Slice data less than n units", c.length < t + n * u); + assertFalse("Slice data exceeds destination", n * u > b.length); + assertFalse("Unit size exceeds spacing", u > p && u + p > 0); + } + +} \ No newline at end of file diff --git a/tests/java/org/python/core/PyBufferTest.java b/tests/java/org/python/core/PyBufferTest.java --- a/tests/java/org/python/core/PyBufferTest.java +++ b/tests/java/org/python/core/PyBufferTest.java @@ -1,17 +1,27 @@ package org.python.core; +import static org.junit.Assert.*; +import static org.python.core.ByteBufferTestSupport.assertIntsEqual; +import static org.python.core.PyBufferTestSupport.bytesFromByteAt; + import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.HashSet; -import java.util.LinkedList; +import java.util.Collection; import java.util.List; -import java.util.Set; -import junit.framework.TestCase; - +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.python.core.ByteBufferTestSupport.ByteMaterial; +import org.python.core.PyBufferTestSupport.ExporterFactory; +import org.python.core.PyBufferTestSupport.ReadonlyExporterFactory; +import org.python.core.PyBufferTestSupport.SlicedTestSpec; +import org.python.core.PyBufferTestSupport.TestSpec; +import org.python.core.PyBufferTestSupport.WritableExporterFactory; import org.python.core.buffer.BaseBuffer; import org.python.core.buffer.SimpleBuffer; import org.python.core.buffer.SimpleStringBuffer; @@ -22,16 +32,18 @@ * Test the several implementations (and exporters) of the PyBuffer interface provided in the Jython * core. *

- * The approach is to create test material once that has the necessary variety in byte array values, - * then for each test, when the JUnit framework creates an instance of the function-specific test, - * to use this material to create instances of each read-only type and each writable type. Writable - * instance types go onto the lists buffersToRead and buffersToWrite, while read-only instances go - * onto the lists buffersToRead and buffersToFailToWrite. + * The approach is to create test material once (static initialisation) that has the necessary + * variety in byte array values. From these raw values, during a phase of (static) initialisation + * invoked by the JUnit framework, we create a rich set of root objects, and slices made from them, + * paired with the value those buffer views should have, represented as byte[] (and a few other + * types). These are BufferTestPair objects. The collection is the test data. *

- * In general, tests of methods that read data apply themselves to all the elements of the - * buffersToRead list, while tests of methods that write data apply themselves to all the elements - * of the buffersToWrite list and check that members of the buffersToFailToWrite list raise an - * exception. + * The JUnit framework will then construct an instance of this test using one + * BufferTestPair object from the test data, and call one test method. The + * initialisation of the test fixture with a BufferTestPair object provides the test + * method with a PyBuffer object on which to operate, and enough ancilliary information + * to deduce the expected outcome. In particular, it will be apparent whether write actions should + * succeed or raise an exception. *

* The Jython buffer API follows the structures of the CPython buffer API so that it supports in * principle the use of multi-dimensional, strided and indirect array structures as buffers. @@ -40,598 +52,499 @@ * N-dimensional cases, and some need a complete re-think. Sub-classing this test would probably be * a good way to extend it to a wider range. */ -public class PyBufferTest extends TestCase { + at RunWith(Parameterized.class) +public class PyBufferTest { /** Control amount of output. Instance variable so can be adjusted temporarily in test. */ protected int verbosity = 0; + /** Print a list of the test material. (From JUnit 4.12 use Parameters(name)). */ + protected static final boolean PRINT_KEY = true; + + /** Size of some large arrays. */ + static final int LONG = 1000; + + /** Lengths we will use if we can when slicing view */ + protected static final int[] sliceLengths = {1, 2, 5, 0, LONG / 4}; + + /** Step sizes we will use if we can when slicing view */ + protected static final int[] sliceSteps = {1, 2, 3, 7}; + + /** Exception raising requires the Jython interpreter to be initialised **/ + protected PythonInterpreter interp = new PythonInterpreter(); + + /** The test material and a buffer created by the test-runner. */ + private TestSpec spec; + ByteMaterial ref; + BufferProtocol obj; + PyBuffer view; + /** - * Generated constructor + * Construct an instance to run one test, using one set of test data. * - * @param name + * @param pair The test material and a buffer created by the test-runner. */ - public PyBufferTest(String name) { - super(name); + public PyBufferTest(TestSpec spec) { + this.spec = spec; + ref = spec.ref; + createObjAndView(); } - /** Sometimes we need the interpreter to be initialised **/ - PythonInterpreter interp; + /** + * Create (or re-create) the test object and view from the specification. Test cases that call a + * mutator repeatedly must call this each time in order to work with clean objects. + */ + protected void createObjAndView() { + TestSpec.ObjectAndView pair = spec.makePair(); + obj = pair.obj; + view = pair.view; + } /* * Values for initialising the exporters. */ - private static final ByteMaterial byteMaterial = new ByteMaterial(0, 16, 17); + private static final ByteMaterial byteMaterial = new ByteMaterial(10, 16, 3); private static final ByteMaterial abcMaterial = new ByteMaterial("abcdefgh"); private static final ByteMaterial stringMaterial = new ByteMaterial("Mon c?t? f?cheux"); private static final ByteMaterial emptyMaterial = new ByteMaterial(new byte[0]); - public static final int LONG = 1000; private static final ByteMaterial longMaterial = new ByteMaterial(0, LONG, 5); - @Override - protected void setUp() throws Exception { - super.setUp(); + /** + * Generate test data to be held in the testing framework and used to construct tests. This + * method is called once by the test framework. Each element of the returned collection is a + * specification that becomes the arguments to the constructor when JUnit prepares to invoke a + * test. + *

+ * Internally, this method creates a small number of instances of the object types whose + * PyBuffer export mechanism is to be tested. Each is paired with a reference value + * represented in several forms. The PyBufferTestSupport class then multiplies + * these by creating a selection of feasible sliced views, the whole collection of root and + * slice objects being returned. + * + * @return generated list of test data + */ + @Parameters + public static Collection genTestSpecs() { - // Exception raising requires the Jython interpreter - interp = new PythonInterpreter(); + PyBufferTestSupport s = new PyBufferTestSupport(sliceLengths, sliceSteps); // Tests using local types of exporter - genWritable(new SimpleWritableExporter(abcMaterial.getBytes()), abcMaterial); - genReadonly(new SimpleExporter(byteMaterial.getBytes()), byteMaterial); - genReadonly(new StringExporter(stringMaterial.string), stringMaterial); - genWritable(new SimpleWritableExporter(emptyMaterial.getBytes()), emptyMaterial); + + ExporterFactory simpleExporter = new ReadonlyExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new SimpleExporter(m.getBytes()); + } + + }; + s.add(simpleExporter, byteMaterial); + + ExporterFactory simpleWritableExporter = new WritableExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new SimpleWritableExporter(m.getBytes()); + } + + }; + s.add(simpleWritableExporter, abcMaterial); + s.add(simpleWritableExporter, emptyMaterial); + + ExporterFactory stringExporter = new ReadonlyExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new StringExporter(m.string); + } + + }; + s.add(stringExporter, stringMaterial); // Tests with PyByteArray - genWritable(new PyByteArray(abcMaterial.getBytes()), abcMaterial); - genWritable(new PyByteArray(longMaterial.getBytes()), longMaterial); - genWritable(new PyByteArray(), emptyMaterial); + + ExporterFactory pyByteArrayExporter = new WritableExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new PyByteArray(m.getBytes()); + } + + }; + s.add(pyByteArrayExporter, byteMaterial); + s.add(pyByteArrayExporter, longMaterial); + s.add(pyByteArrayExporter, emptyMaterial); // Tests with PyString - genReadonly(new PyString(abcMaterial.string), abcMaterial); - genReadonly(new PyString(), emptyMaterial); + + ExporterFactory pyStringExporter = new ReadonlyExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new PyString(m.string); + } + + }; + s.add(pyStringExporter, abcMaterial); + s.add(pyStringExporter, emptyMaterial); // Ensure case is tested where PyByteArray has an internal offset - PyByteArray truncated = new PyByteArray(stringMaterial.getBytes()); - truncated.delRange(0, 4); - ByteMaterial truncatedMaterial = new ByteMaterial(stringMaterial.string.substring(4)); - assert truncated.__alloc__() > truncatedMaterial.length; - genWritable(truncated, truncatedMaterial); + + ExporterFactory offsetPyByteArrayExporter = new WritableExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + // In this PyByteArray the data will not start at storage[0] + final int OFFSET = 4; + byte[] b = m.getBytes(); + // Make a copy with padding at the start and wrap it in a bytearray + byte[] data = new byte[OFFSET + b.length]; + System.arraycopy(b, 0, data, OFFSET, b.length); + PyByteArray a = new PyByteArray(data); + // This operation may (will) be implemented without data movement + a.delRange(0, OFFSET); + assert a.__alloc__() > b.length; + return a; + } + + }; + s.add(offsetPyByteArrayExporter, byteMaterial); + s.add(offsetPyByteArrayExporter, longMaterial); + + // Return the generated test data + + List ret = s.getTestData(); + if (PRINT_KEY) { + int key = 0; + for (TestSpec[] r : ret) { + TestSpec spec = r[0]; + System.out.printf("%6d : %s\n", key++, spec.toString()); + } + } + return ret; } - /** Generate a series of test material for a writable object. */ - private void genWritable(BufferProtocol exporter, ByteMaterial material) { - generate(exporter, material, false); + /** + * Brevity allowing each test to announce itself by naming the part of the api tested. + * + * @param api naming the part of the api tested + */ + protected void announce(String api) { + if (verbosity > 0) { + System.out.printf("%-30s %s\n", api + ":", spec.toString()); + } } - /** Generate a series of test material for a read-only object. */ - private void genReadonly(BufferProtocol exporter, ByteMaterial material) { - generate(exporter, material, true); + /** Test method for {@link org.python.core.PyBUF#isReadonly()}. */ + @Test + public void testIsReadonly() { + announce("isReadonly"); + assertTrue(view.isReadonly() == spec.readonly); } - /** Lengths we will use if we can when slicing view */ - private static final int[] sliceLengths = {1, 2, 5, 0, LONG / 4}; + /** Test method for {@link org.python.core.PyBUF#getNdim()}. */ + @Test + public void testGetNdim() { + announce("getNdim"); + // Only know how to do 1 dimension at the moment + assertEquals("unexpected ndim", spec.shape.length, view.getNdim()); + } - /** Step sizes we will use if we can when slicing view */ - private static final int[] sliceSteps = {1, 2, 3, 7}; + /** Test method for {@link org.python.core.PyBUF#getShape()}. */ + @Test + public void testGetShape() { + announce("getShape"); + int[] shape = view.getShape(); + assertNotNull("shape[] should always be provided", shape); + assertIntsEqual("unexpected shape", spec.shape, shape); + } - /** - * Generate a series of test material for a read-only or writable object. Given one exporter, - * and its reference ByteMaterial this method first queues a BufferTestPair corresponding to the - * exporter as the test subject and its test material. This provides a "direct" PyBuffer view on - * the exporter. It then goes on to make a variety of sliced PyBuffer views of the exporter by - * calling {@link PyBuffer#getBufferSlice(int, int, int, int)} on the direct view. The slices - * are made with a variety of argument combinations, filtered down to those that make sense for - * the size of the direct view. Each sliced buffer (considered a test subject now), together - * with correspondingly sliced reference ByteMaterial is queued as BufferTestPair. - * - * @param exporter underlying object - * @param material reference material corresponding to the exporter - * @param readonly whether the exporter is of read-only type - */ - private void generate(BufferProtocol exporter, ByteMaterial material, boolean readonly) { + /** Test method for {@link org.python.core.PyBUF#getLen()}. */ + @Test + public void testGetLen() { + announce("getLen"); + assertEquals("unexpected length", ref.length, view.getLen()); + } - // Generate a test using the buffer directly exported by the exporter - PyBuffer direct = queue(exporter, material, readonly); + /** Test method for {@link org.python.core.PyBuffer#byteAt(int)}. */ + @Test + public void testByteAt() { + announce("byteAt"); + for (int i = 0; i < ref.length; i++) { + assertEquals(ref.bytes[i], view.byteAt(i)); + } + } - // Generate some slices from the material and this direct view - int N = material.length; - int M = (N + 4) / 4; // At least one and about N/4 + /** Test method for {@link org.python.core.PyBuffer#byteAt(int[])}. */ + @Test + public void testByteAtNdim() { + announce("byteAt (n-dim)"); + int[] index = new int[1]; - // 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 (view.getShape().length != 1) { + fail("Test not implemented if dimensions != 1"); + } + // Run through 1D index for view + for (int i = 0; i < ref.length; i++) { + index[0] = i; + assertEquals(ref.bytes[i], view.byteAt(index)); + } - if (length == 0) { - queue(direct, material, start, 0, 1, readonly); - queue(direct, material, start, 0, 2, readonly); + // Check 2D index throws + try { + view.byteAt(0, 0); + fail("Use of 2D index did not raise exception"); + } catch (PyException pye) { + // Expect BufferError + assertEquals(Py.BufferError, pye.type); + } + } - } else if (length == 1 && start < N) { - queue(direct, material, start, 1, 1, readonly); - queue(direct, material, start, 1, 2, readonly); + /** Test method for {@link org.python.core.PyBuffer#intAt(int)}. */ + @Test + public void testIntAt() { + announce("intAt"); + for (int i = 0; i < ref.length; i++) { + assertEquals(ref.ints[i], view.intAt(i)); + } + } - } else if (start < N) { + /** Test method for {@link org.python.core.PyBuffer#intAt(int[])}. */ + @Test + public void testIntAtNdim() { + announce("intAt (n-dim)"); + int[] index = new int[1]; - // 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); - } - } + if (view.getShape().length != 1) { + fail("Test not implemented for dimensions != 1"); + } + // Run through 1D index for view + for (int i = 0; i < ref.length; i++) { + index[0] = i; + assertEquals(ref.ints[i], view.intAt(index)); + } + // Check 2D index throws + try { + view.intAt(0, 0); + fail("Use of 2D index did not raise exception"); + } catch (PyException pye) { + // Expect BufferError + assertEquals(Py.BufferError, pye.type); + } + } - // 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); - } - } + /** Test method for {@link org.python.core.PyBuffer#storeAt(byte, int)}. */ + @Test + public void testStoreAt() { + announce("storeAt"); + int n = ref.length; + int[] exp = ref.ints.clone(); + if (!spec.readonly) { + // Write modified test material into each location using storeAt() + for (int i = 0; i < n; i++) { + byte v = (byte)(exp[i] ^ 3); // twiddle some bits + view.storeAt(v, i); + } + // Compare each location with modified test data using intAt() + for (int i = 0; i < n; i++) { + assertEquals(exp[i] ^ 3, view.intAt(i)); + } + } else { + // Write should throw + for (int i = 0; i < n; i++) { + try { + view.storeAt((byte)3, i); + fail("Write access not prevented: " + spec); + } catch (PyException pye) { + // Expect TypeError (not BufferError which getBuffer can raise) + assertEquals(Py.TypeError, pye.type); } } } } - /** 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); - if (pair.readonly) { - buffersToFailToWrite.add(pair); - } else { - buffersToWrite.add(pair); - } - } - - /** Read operations should succeed on all these objects. */ - private List buffersToRead = new LinkedList(); - /** Write operations should succeed on all these objects. */ - private List buffersToWrite = new LinkedList(); - /** Write operations should fail on all these objects. */ - private List buffersToFailToWrite = new LinkedList(); - - /** - * A one-dimensional exporter should be able to give us a buffer for all these flag types. - */ - private static final int[] simpleFlags = {PyBUF.SIMPLE, PyBUF.ND, PyBUF.STRIDES, - PyBUF.INDIRECT, PyBUF.FULL_RO}; - - /** To {@link #simpleFlags} we can add any of these */ - private static final int[] simpleTassles = {0, PyBUF.FORMAT, PyBUF.C_CONTIGUOUS, - PyBUF.F_CONTIGUOUS, PyBUF.ANY_CONTIGUOUS}; - - /** - * A one-dimensional exporter with stride!=1 is restricted to give us a buffer only for these - * flag types. - */ - private static final int[] strided1DFlags = {PyBUF.STRIDES, PyBUF.INDIRECT, PyBUF.FULL_RO}; - - /** To {@link #strided1DFlags} we can add any of these */ - private static final int[] strided1DTassles = {0, PyBUF.FORMAT}; - - /** - * Test method for {@link org.python.core.PyBUF#isReadonly()}. - */ - public void testIsReadonly() { - - for (BufferTestPair test : buffersToWrite) { - if (verbosity > 0) { - System.out.println("isReadonly: " + test); - } - assertFalse(test.view.isReadonly()); - } - - for (BufferTestPair test : buffersToFailToWrite) { - if (verbosity > 0) { - System.out.println("isReadonly: " + test); - } - assertTrue(test.view.isReadonly()); - } - } - - /** - * Test method for {@link org.python.core.PyBUF#getNdim()}. - */ - public void testGetNdim() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getNdim: " + test); - } - assertEquals("unexpected ndim", test.shape.length, test.view.getNdim()); - } - } - - /** - * Test method for {@link org.python.core.PyBUF#getShape()}. - */ - public void testGetShape() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getShape: " + test); - } - int[] shape = test.view.getShape(); - assertNotNull("shape[] should always be provided", shape); - assertIntsEqual("unexpected shape", test.shape, shape); - } - } - - /** - * Test method for {@link org.python.core.PyBUF#getLen()}. - */ - public void testGetLen() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getLen: " + test); - } - assertEquals("unexpected length", test.material.length, test.view.getLen()); - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#byteAt(int)}. - */ - public void testByteAt() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("byteAt: " + test); - } - int n = test.material.length; - byte[] exp = test.material.bytes; - for (int i = 0; i < n; i++) { - assertEquals(exp[i], test.view.byteAt(i)); - } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#byteAt(int[])}. - */ - public void testByteAtNdim() { + /** Test method for {@link org.python.core.PyBuffer#storeAt(byte, int[])}. */ + @Test + public void testStoreAtNdim() { + announce("storeAt (n-dim)"); int[] index = new int[1]; - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("byteAt(array): " + test); - } - if (test.view.getShape().length != 1) { - fail("Test not implemented if dimensions != 1"); - } - byte[] exp = test.material.bytes; - int n = test.material.length; - // Run through 1D index for view + int n = ref.length; + int[] exp = ref.ints.clone(); + if (!spec.readonly) { + // Write modified test material into each location using storeAt() for (int i = 0; i < n; i++) { index[0] = i; - assertEquals(exp[i], test.view.byteAt(index)); - } - - // Check 2D index throws - try { - test.view.byteAt(0, 0); - fail("Use of 2D index did not raise exception"); - } catch (PyException pye) { - // Expect BufferError - assertEquals(Py.BufferError, pye.type); - } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#intAt(int)}. - */ - public void testIntAt() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("intAt: " + test); - } - int n = test.material.length; - int[] exp = test.material.ints; - for (int i = 0; i < n; i++) { - assertEquals(exp[i], test.view.intAt(i)); - } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#intAt(int[])}. - */ - public void testIntAtNdim() { - int[] index = new int[1]; - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("intAt(array): " + test); - } - if (test.view.getShape().length != 1) { - fail("Test not implemented for dimensions != 1"); - } - int[] exp = test.material.ints; - int n = test.material.length; - // Run through 1D index for view - for (int i = 0; i < n; i++) { - index[0] = i; - assertEquals(exp[i], test.view.intAt(index)); - } - // Check 2D index throws - try { - test.view.intAt(0, 0); - fail("Use of 2D index did not raise exception"); - } catch (PyException pye) { - // Expect BufferError - assertEquals(Py.BufferError, pye.type); - } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#storeAt(byte, int)}. - */ - public void testStoreAt() { - for (BufferTestPair test : buffersToWrite) { - if (verbosity > 0) { - System.out.println("storeAt: " + test); - } - int n = test.material.length; - int[] exp = test.material.ints; - // Write modified test material into each location using storeAt() - for (int i = 0; i < n; i++) { - byte v = (byte)(exp[i] ^ 3); // twiddle some bits - test.view.storeAt(v, i); + byte v = (byte)(exp[i] ^ 3); // twiddle some bits + view.storeAt(v, index); } // Compare each location with modified test data using intAt() for (int i = 0; i < n; i++) { - assertEquals(exp[i] ^ 3, test.view.intAt(i)); + index[0] = i; + assertEquals(exp[i] ^ 3, view.intAt(index)); } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#storeAt(byte, int[])}. - */ - public void testStoreAtNdim() { - for (BufferTestPair test : buffersToWrite) { - if (verbosity > 0) { - System.out.println("storeAt: " + test); + if (spec.shape.length == 1) { + // Check 2D index throws + try { + view.storeAt((byte)1, 0, 0); + fail("Use of 2D index did not raise exception"); + } catch (PyException pye) { + // Expect BufferError + // XXX ... but should it be TypeError here? + assertEquals(Py.BufferError, pye.type); + } } - int n = test.material.length; - int[] exp = test.material.ints; - // Write modified test material into each location using storeAt() + } else { + // Write should throw for (int i = 0; i < n; i++) { - byte v = (byte)(exp[i] ^ 3); // twiddle some bits - test.view.storeAt(v, i); - } - // Compare each location with modified test data using intAt() - for (int i = 0; i < n; i++) { - assertEquals(exp[i] ^ 3, test.view.intAt(i)); - } - // Check 2D index throws - try { - test.view.storeAt((byte)1, 0, 0); - fail("Use of 2D index did not raise exception"); - } catch (PyException pye) { - // Expect BufferError - assertEquals(Py.BufferError, pye.type); - } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#copyTo(byte[], int)}. - */ - public void testCopyTo() { - final int OFFSET = 5; - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("copyTo: " + test); - } - int n = test.material.length; - // Try with zero offset - byte[] actual = new byte[n]; - test.view.copyTo(actual, 0); - assertBytesEqual("copyTo() incorrect", test.material.bytes, actual, 0); - // Try to middle of array - actual = new byte[n + 2 * OFFSET]; - test.view.copyTo(actual, OFFSET); - assertBytesEqual("copyTo(offset) incorrect", test.material.bytes, actual, OFFSET); - assertEquals("data before destination", 0, actual[OFFSET - 1]); - assertEquals("data after destination", 0, actual[OFFSET + n]); - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#copyTo(int, byte[], int, int)}. - */ - public void testSliceCopyTo() { - final int OFFSET = 5; - final byte BLANK = 7; - - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("copyTo(from slice): " + test); - } - PyBuffer view = test.view; - - int n = test.material.length; - byte[] actual = new byte[n + 2 * OFFSET]; - - // Try destination positions in actual[] of 0 and OFFSET - for (int destPos = 0; destPos <= OFFSET; destPos += OFFSET) { - // Try source positions in 0 and OFFSET - for (int srcIndex = 0; srcIndex <= OFFSET; srcIndex += OFFSET) { - - // A variety of lengths from zero to (n-srcIndex)-ish - for (int length = 0; srcIndex + length <= n; length = 2 * length + 1) { - - if (verbosity > 1) { - System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", - srcIndex, srcIndex + length, n, destPos, destPos + length, - actual.length); - } - - Arrays.fill(actual, BLANK); - - // Test the method - view.copyTo(srcIndex, actual, destPos, length); - - // Check changed part of destination - assertBytesEqual("copyTo(slice) incorrect", test.material.bytes, srcIndex, - length, actual, destPos); - if (destPos > 0) { - assertEquals("data before destination", BLANK, actual[destPos - 1]); - } - assertEquals("data after destination", BLANK, actual[destPos + length]); - } - - // And from exactly n-srcIndex down to zero-ish - for (int trim = 0; srcIndex + trim <= n; trim = 2 * trim + 1) { - int length = n - srcIndex - trim; - - if (verbosity > 1) { - System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", - srcIndex, srcIndex + length, n, destPos, destPos + length, - actual.length); - } - - Arrays.fill(actual, BLANK); - - // Test the method - view.copyTo(srcIndex, actual, destPos, length); - - // Check changed part of destination - assertBytesEqual("copyTo(slice) incorrect", test.material.bytes, srcIndex, - length, actual, destPos); - if (destPos > 0) { - assertEquals("data before destination", BLANK, actual[destPos - 1]); - } - assertEquals("data after destination", BLANK, actual[destPos + length]); - } + index[0] = i; + try { + view.storeAt((byte)3, index); + fail("Write access not prevented: " + spec); + } catch (PyException pye) { + // Expect TypeError (not BufferError which getBuffer can raise) + assertEquals(Py.TypeError, pye.type); } } } } - /** - * Test method for {@link org.python.core.PyBuffer#copyFrom(byte[], int, int, int)}. - */ + /** Test method for {@link org.python.core.PyBuffer#copyTo(byte[], int)}. */ + @Test + public void testCopyTo() { + final int OFFSET = 5; + announce("copyTo"); + int n = ref.length; + // Try with zero offset + byte[] actual = new byte[n]; + view.copyTo(actual, 0); + ByteBufferTestSupport.assertBytesEqual("copyTo() incorrect", ref.bytes, actual, 0); + // Try to middle of array + actual = new byte[n + 2 * OFFSET]; + view.copyTo(actual, OFFSET); + ByteBufferTestSupport.assertBytesEqual("copyTo(offset) incorrect", ref.bytes, actual, + OFFSET); + assertEquals("data before destination", 0, actual[OFFSET - 1]); + assertEquals("data after destination", 0, actual[OFFSET + n]); + } + + /** Test method for {@link org.python.core.PyBuffer#copyTo(int, byte[], int, int)}. */ + @Test + public void testSliceCopyTo() { + announce("copyTo (from slice)"); + final int OFFSET = 3; + + int n = ref.length; + byte[] before = new byte[n + 2 * OFFSET]; + final byte BLANK = 7; + Arrays.fill(before, BLANK); + + // Try destination positions in actual[] of 0 and OFFSET + for (int destPos = 0; destPos <= OFFSET; destPos += OFFSET) { + // Try source positions in 0 and OFFSET + for (int srcIndex = 0; srcIndex <= OFFSET; srcIndex += OFFSET) { + + // A variety of lengths from zero to (n-srcIndex)-ish + for (int length = 0; srcIndex + length <= n; length = 2 * length + 1) { + doTestSliceCopyTo(srcIndex, before, destPos, length, n); + } + + // And from exactly n-srcIndex down to zero-ish + for (int trim = 0; srcIndex + trim <= n; trim = 2 * trim + 1) { + int length = n - srcIndex - trim; + doTestSliceCopyTo(srcIndex, before, destPos, length, n); + } + } + } + } + + /** Helper function for {@link #testSliceCopyTo()} */ + private void doTestSliceCopyTo(int srcIndex, byte[] before, int destPos, int length, int n) { + + if (verbosity > 1) { + System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", srcIndex, srcIndex + + length, n, destPos, destPos + length, before.length); + } + + // Test the method + byte[] dest = before.clone(); + view.copyTo(srcIndex, dest, destPos, length); + + // Check the write to dest contains a correctly positioned copy of the view (=ref.bytes) + byte[] viewBytes = PyBufferTestSupport.bytesFromByteAt(view); + ByteBufferTestSupport.checkReadCorrect(ref.bytes, viewBytes, dest, destPos, length, 1, + srcIndex, 1); + + } + + /** Test method for {@link org.python.core.PyBuffer#copyFrom(byte[], int, int, int)}. */ + @Test public void testCopyFrom() { - final int OFFSET = 5; - final byte BLANK = 7; + announce("copyFrom"); + final int OFFSET = 3; + final int L = ref.length; - for (BufferTestPair test : buffersToWrite) { - if (verbosity > 0) { - System.out.println("copyFrom(): " + test); - } - PyBuffer view = test.view; + // Make some source material to copy from (longer since need to test at OFFSET too). + byte[] src = (new ByteMaterial(48, ref.length + OFFSET, 1)).bytes; - int n = test.material.length; - byte[] actual = new byte[n]; - byte[] expected = new byte[n]; + // Our test is against the underlying object of which the view may be a slice + TestSpec underlying = spec.getOriginal(); + int start = spec.getStart(); + int stride = spec.getStride(); - // Make some source material for copies (need to test at OFFSET too). - byte[] src = new byte[n + OFFSET]; - for (int i = 0; i < src.length; i++) { - src[i] = (byte)i; - } + // Try source positions in 0 and OFFSET + for (int srcPos = 0; srcPos <= OFFSET; srcPos += OFFSET) { // Try destination positions in test object of 0 and OFFSET for (int destIndex = 0; destIndex <= OFFSET; destIndex += OFFSET) { - // Try source positions in 0 and OFFSET - for (int srcPos = 0; srcPos <= OFFSET; srcPos += OFFSET) { + // A variety of lengths from zero to (n-destIndex)-ish + for (int length = 0; destIndex + length <= L; length = 2 * length + 1) { + doTestCopyFrom(src, srcPos, underlying, start, length, stride, destIndex); + } - // A variety of lengths from zero to (n-destIndex)-ish - for (int length = 0; destIndex + length <= n; length = 2 * length + 1) { + // And from exactly n-destIndex down to zero-ish + for (int trim = 0; destIndex + trim <= L; trim = 2 * trim + 1) { + int length = ref.length - destIndex - trim; + doTestCopyFrom(src, srcPos, underlying, start, length, stride, destIndex); + } + } + } + } - 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); - } + /** Helper function for {@link #testCopyFrom()} */ + private void doTestCopyFrom(byte[] src, int srcPos, TestSpec underlying, int start, int length, + int stride, int destIndex) { - // Initialise the object (have to do each time) and expected value - for (int i = 0; i < n; i++) { - expected[i] = BLANK; - view.storeAt(BLANK, i); - } + if (verbosity > 1) { + System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d]\n", srcPos, srcPos + length, + ref.length, destIndex, destIndex + length); + } - // Test the method and extract the result to actual[] - view.copyFrom(src, srcPos, destIndex, length); - view.copyTo(actual, 0); + // Initialise the object (have to do each time) + createObjAndView(); + PyBuffer underlyingView = obj.getBuffer(underlying.flags & ~PyBUF.WRITABLE); + byte[] before = bytesFromByteAt(underlyingView); - // Complete what is should be in expected[] - for (int i = 0; i < length; i++) { - expected[destIndex + i] = src[srcPos + i]; - } - assertBytesEqual("copyFrom() incorrect", expected, actual, 0); - } + if (!spec.readonly) { + // This is the call we are testing (a write operation). + view.copyFrom(src, srcPos, destIndex, length); - // And from exactly n-destIndex down to zero-ish - for (int trim = 0; destIndex + trim <= n; trim = 2 * trim + 1) { - int length = n - destIndex - trim; + // Our test is against the underlying object of which the view may be a slice + byte[] after = bytesFromByteAt(underlyingView); + int underlyingDestIndex = start + destIndex * stride; - 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); - } + // Test that the corresponding bytes of the underlying object match data copied in + ByteBufferTestSupport.checkWriteCorrect(before, after, src, srcPos, length, 1, + underlyingDestIndex, stride); - // Initialise the object (have to do each time) and expected value - for (int i = 0; i < n; i++) { - expected[i] = BLANK; - view.storeAt(BLANK, i); - } - - // Test the method and extract the result to actual[] - view.copyFrom(src, srcPos, destIndex, length); - view.copyTo(actual, 0); - - // Complete what is should be in expected[] - for (int i = 0; i < length; i++) { - expected[destIndex + i] = src[srcPos + i]; - } - assertBytesEqual("copyFrom() incorrect", expected, actual, 0); - } - } + } else { + // Should fail (write operation) + try { + view.copyFrom(src, srcPos, destIndex, length); + fail("Write access not prevented: " + spec); + } catch (PyException pye) { + // Expect TypeError only if the buffer was readonly + assertEquals(Py.TypeError, pye.type); } } } @@ -640,161 +553,106 @@ * Test method for {@link org.python.core.BufferProtocol#getBuffer()} and * {@link org.python.core.PyBuffer#getBuffer()}. */ - public void testGetBuffer() { + public void testGetBufferForRead() { + announce("getBuffer(READ): "); + // Go through all the allowed combinations of flags and tassles + for (int flags : spec.validFlags) { + for (int tassles : spec.validTassles) { + PyBuffer view2 = view.getBuffer(flags | tassles); + assertNotNull(view2); + } + } + } - 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); + /** + * Test method for {@link org.python.core.BufferProtocol#getBuffer()} and + * {@link org.python.core.PyBuffer#getBuffer()}. + */ + public void testGetBufferForWrite() { + announce("getBuffer(WRITE): "); + if (!spec.readonly) { + // Go through all the allowed combinations of flags and tassles adding WRITABLE + for (int flags : spec.validFlags) { + for (int tassles : spec.validTassles) { + PyBuffer view2 = view.getBuffer(flags | tassles | PyBUF.WRITABLE); + assertNotNull(view2); } } - } - - for (BufferTestPair test : buffersToWrite) { - if (verbosity > 0) { - System.out.println("getBuffer(WRITABLE): " + test); - } - for (int flags : test.validFlags) { - for (int tassle : test.validTassles) { - PyBuffer view = test.subject.getBuffer(flags | tassle | PyBUF.WRITABLE); - assertNotNull(view); - } - } - } - - for (BufferTestPair test : buffersToFailToWrite) { - if (verbosity > 0) { - System.out.println("getBuffer(WRITABLE): " + test); - } - for (int flags : test.validFlags) { + } else { + // Go through all the allowed combinations of flags adding WRITABLE + for (int flags : spec.validFlags) { try { - test.subject.getBuffer(flags | PyBUF.WRITABLE); - fail("Write access not prevented: " + test); + view.getBuffer(flags | PyBUF.WRITABLE); + fail("Write access not prevented: " + spec); } catch (PyException pye) { // Expect BufferError assertEquals(Py.BufferError, pye.type); } } } - } /** * Test method for {@link org.python.core.PyBUF#release()}, exercising the release semantics of - * PyBuffer. + * PyBuffer in the try-with-resources pattern. */ - public void testRelease() { + @Test + public void testReleaseTryWithResources() { + announce("release (via try)"); + /* + * this.obj is an actual exporter and this.view is a buffer view onto it. + */ + int flags = PyBUF.STRIDES | PyBUF.FORMAT; - /* - * Testing the semantics of release() is tricky when it comes to 'final' release behaviour. - * We'd like to test that buffers can be acquired and released, that "over release" is - * detected as an error, and that after final release of the buffer (where the export count - * becomes zero) an exporter remains capable of exporting again. Each test is constructed - * with a subject and a view on the subject (if the subject is an exporter), so you might - * think the export count would be one in every case. Two problems: in many tests, the - * subject is a PyBuffer, which has the option (if it would work) to return itself; and a - * PyBuffer is not expected to provide a new buffer view once finally released. - */ - - Set uniqueBuffers = new HashSet(); - - // Test a balanced sequence of acquire and release using try-with-resources - for (BufferTestPair test : buffersToRead) { - doTestTryWithResources(test); + // The test setup should guarantee view is the only export + try (PyBuffer c = obj.getBuffer(flags)) { // = 2 exports + try (PyBuffer b = obj.getBuffer(PyBUF.FULL_RO); PyBuffer d = c.getBuffer(flags)) { + maybeCheckExporting(obj);// = 4 exports + } + maybeCheckExporting(obj); // = 2 exports + throw new Throwable("test"); + } catch (Throwable e) { + // Meh } - - // Now test a pattern of acquire and release with one more release than acquire - for (BufferTestPair test : buffersToRead) { - doTestRelease(test); - uniqueBuffers.add(test.view); - } - - // All buffers are released: test that any further release is detected as an error. - for (PyBuffer view : uniqueBuffers) { - doTestOverRelease(view); - } - - // All exporters are currently not exporting buffers - for (BufferTestPair test : buffersToRead) { - if (!(test.subject instanceof PyBuffer)) { - doTestGetAfterRelease(test); - } - } - + maybeCheckExporting(obj); // = 1 export + view.release(); + maybeCheckNotExporting(obj); // = 0 exports } /** - * Exercise try-with-resources on one BufferTestPair. + * Test method for {@link org.python.core.PyBUF#release()}, exercising release semantics in a + * sequence orchestrated by the client code. At the end, the view should be fully released, ( + * {@link PyBuffer#isReleased()}==true). */ - private void doTestTryWithResources(BufferTestPair test) { + @Test + public void testRelease() { + announce("release"); + int flags = PyBUF.STRIDES | PyBUF.FORMAT; - if (verbosity > 0) { - System.out.println("try with resources: " + test); - } - int flags = PyBUF.STRIDES | PyBUF.FORMAT; - BufferProtocol sub = test.subject; - - // The object will be exporting test.view and N other views we don't know about - try (PyBuffer c = sub.getBuffer(flags)) { // = N+1 exports - try (PyBuffer b = sub.getBuffer(PyBUF.FULL_RO); PyBuffer d =c.getBuffer(flags)) { - checkExporting(sub);// = N+3 exports - } - checkExporting(sub); // = N+1 exports - } - checkExporting(sub); // = N export - } - - /** - * Exercise the release semantics of one BufferTestPair. At the end, the view in the - * BufferTestPair should be fully released, ({@link PyBuffer#isReleased()}==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); + // The object will be exporting view only + PyBuffer a = view; // = 1 exports + PyBuffer b = obj.getBuffer(PyBUF.FULL_RO); // = 2 export + PyBuffer c = obj.getBuffer(flags); // = 3 exports + maybeCheckExporting(obj); // Now see that releasing in some other order works correctly - b.release(); // = N+2 exports - a.release(); // = N+1 exports - checkExporting(sub); + b.release(); // = 2 exports + a.release(); // = 1 exports + maybeCheckExporting(obj); // You can get a buffer from a buffer (c is unreleased) - PyBuffer d = c.getBuffer(flags); // = N+2 exports - c.release(); // = N+1 export - checkExporting(sub); - d.release(); // = N exports - } - - /** - * The view argument should be a fully released buffer, ({@link PyBuffer#isReleased()} - * ==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()); + PyBuffer d = c.getBuffer(flags); // = 2 exports + c.release(); // = 1 export + maybeCheckExporting(obj); + d.release(); // = no exports // Further releases are an error try { - view.release(); // = -1 exports (oops) + view.release(); // = -1 exports (oops) fail("excess release not detected"); } catch (Exception e) { // Success } - } /** @@ -803,41 +661,40 @@ * We check this is true, and that a new buffer may still be acquired from the real object, but * not from the released buffer. */ - private void doTestGetAfterRelease(BufferTestPair test) { + @Test + public void testGetAfterRelease() { + announce("getBuffer (after release)"); - if (verbosity > 0) { - System.out.println("get again: " + test); - } - BufferProtocol sub = test.subject; + // The test objects should have exactly one export + view.release(); - // Fail here if doTestRelease did not fully release, or - checkNotExporting(sub); + // The view can be checked, but not always the obj + maybeCheckNotExporting(obj); + maybeCheckNotExporting(view); // Further gets via the released buffer are an error try { - test.view.getBuffer(PyBUF.FULL_RO); + view.getBuffer(PyBUF.FULL_RO); fail("PyBuffer.getBuffer after final release not detected"); } catch (Exception e) { // Detected *and* prevented? - checkNotExporting(sub); + maybeCheckNotExporting(obj); } // And so are sliced gets try { - test.view.getBufferSlice(PyBUF.FULL_RO, 0, 0); + view.getBufferSlice(PyBUF.FULL_RO, 0, 0); fail("PyBuffer.getBufferSlice after final release not detected"); } catch (Exception e) { // Detected *and* prevented? - checkNotExporting(sub); + maybeCheckNotExporting(obj); } - /* - * Even after some abuse, we can still get and release a buffer. - */ - PyBuffer b = sub.getBuffer(PyBUF.FULL_RO); // = 1 export - checkExporting(sub); - b.release(); // = 0 exports - checkNotExporting(sub); + // Even after some abuse, we can still get and release a buffer. + PyBuffer b = obj.getBuffer(PyBUF.FULL_RO); // = 1 export + maybeCheckExporting(obj); + b.release(); // = 0 exports + maybeCheckNotExporting(obj); } /** @@ -846,7 +703,7 @@ * * @param subject */ - private void checkExporting(BufferProtocol subject) { + private void maybeCheckExporting(BufferProtocol subject) { if (subject instanceof TestableExporter) { assertTrue("exports not being counted", ((TestableExporter)subject).isExporting()); } else if (subject instanceof PyBuffer) { @@ -869,7 +726,7 @@ * * @param subject */ - private void checkNotExporting(BufferProtocol subject) { + private void maybeCheckNotExporting(BufferProtocol subject) { if (subject instanceof TestableExporter) { assertFalse("exports counted incorrectly", ((TestableExporter)subject).isExporting()); } else if (subject instanceof PyBuffer) { @@ -887,225 +744,176 @@ // Other types cannot be checked } - /** - * Test method for {@link org.python.core.PyBuffer#getBufferSlice(int, int, int, int)}. - */ + /** Test method for {@link org.python.core.PyBuffer#getBufferSlice(int, int, int, int)}. */ + @Test public void testGetBufferSliceWithStride() { + announce("getBuffer (slice & stride)"); - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getBufferSliceWithStride: " + test); - } - ByteMaterial material = test.material; - PyBuffer view = test.view; - boolean readonly = test.readonly; + // Generate some slices from the material and the test view + int N = ref.length; + int M = (N + 4) / 4; // At least one and about N/4 - // Generate some slices from the material and the test view - int N = material.length; - int M = (N + 4) / 4; // At least one and about N/4 + // For a range of start positions up to one beyond the end + for (int start = 0; start <= N; start += M) { + // For a range of lengths + for (int length : sliceLengths) { - // For a range of start positions up to one beyond the end - for (int start = 0; start <= N; start += M) { - // For a range of lengths - for (int length : sliceLengths) { + if (length == 0) { + doTestGetBufferSliceWithStride(start, 0, 1); + doTestGetBufferSliceWithStride(start, 0, 2); - if (length == 0) { - checkSlice(view, material, start, 0, 1, readonly); - checkSlice(view, material, start, 0, 2, readonly); + } else if (length == 1 && start < N) { + doTestGetBufferSliceWithStride(start, 1, 1); + doTestGetBufferSliceWithStride(start, 1, 2); - } else if (length == 1 && start < N) { - checkSlice(view, material, start, 1, 1, readonly); - checkSlice(view, material, start, 1, 2, readonly); + } else if (start < N) { - } else if (start < N) { + // And for a range of step sizes + for (int step : sliceSteps) { + // Check this is a feasible slice + if (start + (length - 1) * step < N) { + doTestGetBufferSliceWithStride(start, length, step); + } + } - // And for a range of step sizes - for (int step : sliceSteps) { - // Check this is a feasible slice - if (start + (length - 1) * step < N) { - checkSlice(view, material, start, length, step, readonly); - } - } - - // Now use all the step sizes negatively - for (int step : sliceSteps) { - // Check this is a feasible slice - if (start - (length - 1) * step >= 0) { - checkSlice(view, material, start, length, -step, readonly); - } + // Now use all the step sizes negatively + for (int step : sliceSteps) { + // Check this is a feasible slice + if (start - (length - 1) * step >= 0) { + doTestGetBufferSliceWithStride(start, length, -step); } } } } } - } /** * Helper for {@link #testGetBufferSliceWithStride()} that obtains one sliced buffer to * specification and checks it against the material. */ - private void checkSlice(PyBuffer view, ByteMaterial material, int start, int length, int step, - boolean readonly) { + private void doTestGetBufferSliceWithStride(int first, int count, int step) { - int flags = readonly ? PyBUF.FULL_RO : PyBUF.FULL; + // view is a view matching ref.bytes. Make a reference value for a further slice. + TestSpec slicedSpec = new SlicedTestSpec(spec, spec.getItemsize(), first, count, step); if (verbosity > 1) { - System.out.printf(" checkSlice: start=%4d, length=%4d, step=%4d \n", start, length, - step); + System.out.printf( + " slice first=%4d, count=%4d, step=%4d -> underlying start=%4d, stride=%4d\n", + first, count, step, slicedSpec.getStart(), slicedSpec.getStride()); } - byte[] expected = sliceBytes(material.bytes, start, length, step); - PyBuffer sliceView = view.getBufferSlice(flags, start, length, step); - byte[] result = bytesFromByteAt(sliceView); - assertBytesEqual(" testGetBufferSliceWithStride failure: ", expected, result); + // Now compute that further slice using the library under test (not makePair) + PyBuffer slicedView = view.getBufferSlice(spec.flags, first, count, step); + byte[] slice = PyBufferTestSupport.bytesFromByteAt(slicedView); + + // Did we get the same as the reference material in the + ByteBufferTestSupport.assertBytesEqual("slice incorrect", slicedSpec.ref.bytes, slice); } - /** - * Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer()}. - */ + + /** Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer()}. */ + @Test public void testGetNIOByteBuffer() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getNIOByteBuffer: " + test); + announce("getNIOByteBuffer"); + int stride = spec.getStride(); + ByteBuffer bb = view.getNIOByteBuffer(); + ByteBufferTestSupport.assertBytesEqual("buffer does not match reference", ref.bytes, bb, + stride); + if (spec.readonly) { + assertTrue("ByteBuffer should be read-only", bb.isReadOnly()); + } else { + assertFalse("ByteBuffer should not be read-only", bb.isReadOnly()); + } + + } + + /** Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer(int)}. */ + @Test + public void testGetNIOByteBufferInt() { + announce("getNIOByteBuffer (int)"); + + int n = ref.length, itemsize = view.getItemsize(); + byte[] exp = new byte[itemsize], bytes = ref.bytes; + + for (int i = 0; i < n; i++) { + // Expected result is one item (allow for itemsize) + int p = i * itemsize; + for (int j = 0; j < itemsize; j++) { + exp[j] = bytes[p + j]; } - int stride = test.strides[0]; - - 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); - - ByteBuffer bp = view.getNIOByteBuffer(); - 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 - ByteBuffer bp = view.getNIOByteBuffer(); - assertBytesEqual("buffer does not match reference", test.material.bytes, bp, stride); - } - + // Get buffer and check for correct data at bb.position() + ByteBuffer bb = view.getNIOByteBuffer(i); + ByteBufferTestSupport.assertBytesEqual("getNIOByteBuffer(int) value", exp, bb); } } - /** - * Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer(int)}. - */ - public void testGetNIOByteBuffer_int() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getNIOByteBuffer(int): " + test); + /** Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer(int[])}. */ + @Test + public void testGetNIOByteBufferIntArray() { + int[] index = new int[1]; + announce("getNIOByteBuffer (n-dim)"); + + int n = ref.length, itemsize = view.getItemsize(); + byte[] exp = new byte[itemsize], bytes = ref.bytes; + + for (int i = 0; i < n; i++) { + // Expected result is one item (allow for itemsize) + int p = i * itemsize; + for (int j = 0; j < itemsize; j++) { + exp[j] = bytes[p + j]; } - PyBuffer view = test.view; - int n = test.material.length, itemsize = view.getItemsize(); - byte[] exp = new byte[itemsize], bytes = test.material.bytes; - for (int i = 0; i < n; i++) { - // Expected result is one item (allow for itemsize) - int p = i * itemsize; - for (int j = 0; j < itemsize; j++) { - exp[j] = bytes[p + j]; - } + // Get buffer and check for correct data at bb.position() + index[0] = i; + ByteBuffer bb = view.getNIOByteBuffer(index); + ByteBufferTestSupport.assertBytesEqual("getNIOByteBuffer(int[]) value", exp, bb); + } - // Get buffer and check for correct data at bb.position() - ByteBuffer bb = view.getNIOByteBuffer(i); - assertBytesEqual("getNIOByteBuffer(int) value", exp, bb); - } + // Check 2D index throws + try { + view.getNIOByteBuffer(0, 0); + fail("Use of 2D index did not raise exception"); + } catch (PyException pye) { + // Expect BufferError + // XXX ... but should it be TypeError here? + assertEquals(Py.BufferError, pye.type); } } - /** - * Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer(int[])}. - */ - public void testGetNIOByteBuffer_intArray() { - int[] index = new int[1]; - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getNIOByteBuffer(int[]): " + test); - } - PyBuffer view = test.view; - int n = test.material.length, itemsize = view.getItemsize(); - byte[] exp = new byte[itemsize], bytes = test.material.bytes; - - for (int i = 0; i < n; i++) { - // Expected result is one item (allow for itemsize) - int p = i * itemsize; - for (int j = 0; j < itemsize; j++) { - exp[j] = bytes[p + j]; - } - - // Get buffer and check for correct data at bb.position() - index[0] = i; - ByteBuffer bb = view.getNIOByteBuffer(index); - assertBytesEqual("getNIOByteBuffer(int[]) value", exp, bb); - } - - // Check 2D index throws - try { - view.getNIOByteBuffer(0, 0); - fail("Use of 2D index did not raise exception"); - } catch (PyException pye) { - // Expect BufferError - assertEquals(Py.BufferError, pye.type); - } + /** Test method for {@link org.python.core.PyBuffer#hasArray()}. */ + @Test + public void testHasArray() { + announce("hasArray"); + if (spec.hasArray) { + assertTrue("a backing array was expected", view.hasArray()); + } else { + assertFalse("no backing array was expected", view.hasArray()); } } - - - /** - * Test method for {@link org.python.core.PyBuffer#getBuf()}. - */ + /** Test method for {@link org.python.core.PyBuffer#getBuf()}. */ + @Test @SuppressWarnings("deprecation") public void testGetBuf() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getBuf: " + test); - } - int stride = test.strides[0]; - - if (stride == 1) { - - // The client should not have to support navigation with the strides array - int flags = test.readonly ? PyBUF.SIMPLE : PyBUF.SIMPLE + PyBUF.WRITABLE; - PyBuffer view = test.subject.getBuffer(flags); - - PyBuffer.Pointer bp = view.getBuf(); - assertBytesEqual("buffer does not match reference", test.material.bytes, bp); - - } else { - - // The client will have to navigate with the strides array - int flags = test.readonly ? PyBUF.STRIDED_RO : PyBUF.STRIDED; - PyBuffer view = test.subject.getBuffer(flags); - - stride = view.getStrides()[0]; // Just possibly != test.strides when length<=1 - PyBuffer.Pointer bp = view.getBuf(); - assertBytesEqual("buffer does not match reference", test.material.bytes, bp, stride); - } - + announce("getBuf"); + if (spec.hasArray) { + int stride = spec.getStride(); + PyBuffer.Pointer bp = view.getBuf(); + assertBytesEqual("buffer does not match reference", ref.bytes, bp, stride); } } - /** - * Test method for {@link org.python.core.PyBuffer#getPointer(int)}. - */ + /** Test method for {@link org.python.core.PyBuffer#getPointer(int)}. */ + @Test @SuppressWarnings("deprecation") public void testGetPointer() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getPointer: " + test); - } - PyBuffer view = test.view; - int n = test.material.length, itemsize = view.getItemsize(); - byte[] exp = new byte[itemsize], bytes = test.material.bytes; + announce("getPointer"); + if (spec.hasArray) { + int itemsize = spec.getItemsize(); + byte[] exp = new byte[itemsize], bytes = ref.bytes; - for (int i = 0; i < n; i++) { + // Try to get a pointer to an item at each byte location in the buffer + for (int i = 0; i <= ref.length - itemsize; i++) { // Expected result is one item (allow for itemsize) int p = i * itemsize; for (int j = 0; j < itemsize; j++) { @@ -1119,19 +927,15 @@ } } - /** - * Test method for {@link org.python.core.PyBuffer#getPointer(int[])}. - */ + /** Test method for {@link org.python.core.PyBuffer#getPointer(int[])}. */ + @Test @SuppressWarnings("deprecation") public void testGetPointerNdim() { int[] index = new int[1]; - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getPointer(array): " + test); - } - PyBuffer view = test.view; - int n = test.material.length, itemsize = view.getItemsize(); - byte[] exp = new byte[itemsize], bytes = test.material.bytes; + announce("getPointer(array)"); + if (spec.hasArray) { + int n = ref.length, itemsize = view.getItemsize(); + byte[] exp = new byte[itemsize], bytes = ref.bytes; for (int i = 0; i < n; i++) { // Expected result is one item (allow for itemsize) @@ -1157,104 +961,111 @@ } } - /** - * Test method for {@link org.python.core.PyBUF#getStrides()}. - */ + /** Test method for {@link org.python.core.PyBUF#getStrides()}. */ + @Test public void testGetStrides() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getStrides: " + test); - } - for (int flags : test.validFlags) { - PyBuffer view = test.subject.getBuffer(flags); - // Strides array irrespective of the client flags ... (different from CPython) - int[] strides = view.getStrides(); - assertNotNull("strides[] should always be provided", strides); - - // The strides must have the expected value if length >1 - if (test.material.bytes.length > 1) { - assertIntsEqual("unexpected strides", test.strides, strides); - } + announce("getStrides"); + for (int flags : spec.validFlags) { + PyBuffer view = this.view.getBuffer(flags); + // Strides array irrespective of the client flags ... (different from CPython) + int[] strides = view.getStrides(); + assertNotNull("strides[] should always be provided", strides); + // The strides must have the expected value if length >1 + if (ref.bytes.length > 1) { + assertIntsEqual("unexpected strides", spec.strides, strides); } } } - /** - * Test method for {@link org.python.core.PyBUF#getSuboffsets()}. - */ + /** Test method for {@link org.python.core.PyBUF#getSuboffsets()}. */ + @Test public void testGetSuboffsets() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getSuboffsets: " + test); - } - // Null for all test material - assertNull(test.view.getSuboffsets()); - } + announce("getSuboffsets"); + // Null for all test material + assertNull(view.getSuboffsets()); + } - /** - * Test method for {@link org.python.core.PyBUF#isContiguous(char)}. - */ + /** Test method for {@link org.python.core.PyBUF#isContiguous(char)}. */ + @Test public void testIsContiguous() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("isContiguous: " + test); - } - // True for all test material and orders (since 1-dimensional) - for (String orderMsg : validOrders) { - char order = orderMsg.charAt(0); - assertTrue(orderMsg, test.view.isContiguous(order)); - } + announce("isContiguous"); + // True for all test material and orders (since 1-dimensional) + for (String orderMsg : validOrders) { + char order = orderMsg.charAt(0); + assertTrue(orderMsg, view.isContiguous(order)); } } private static final String[] validOrders = {"C-contiguous test fail", "F-contiguous test fail", "Any-contiguous test fail"}; - /** - * Test method for {@link org.python.core.PyBuffer#getFormat()}. - */ + /** Test method for {@link org.python.core.PyBuffer#getFormat()}. */ + @Test public void testGetFormat() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getFormat: " + test); - } - for (int flags : test.validFlags) { - PyBuffer view = test.subject.getBuffer(flags); - // Format given irrespective of the client flags ... (different from CPython) - assertNotNull("format should always be provided", view.getFormat()); - assertEquals("B", view.getFormat()); - // And, we can ask for it explicitly ... - view = test.subject.getBuffer(flags | PyBUF.FORMAT); - assertEquals("B", view.getFormat()); - } + announce("getFormat"); + TestSpec spec = this.spec; + + for (int flags : spec.validFlags) { + PyBuffer view = this.view.getBuffer(flags); + // Format given irrespective of the client flags ... (different from CPython) + assertNotNull("format should always be provided", view.getFormat()); + assertEquals("B", view.getFormat()); + // And, we can ask for it explicitly ... + view = this.view.getBuffer(flags | PyBUF.FORMAT); + assertEquals("B", view.getFormat()); } } - /** - * Test method for {@link org.python.core.PyBUF#getItemsize()}. - */ + /** Test method for {@link org.python.core.PyBUF#getItemsize()}. */ + @Test public void testGetItemsize() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getItemsize: " + test); - } - // Unity for all test material - assertEquals(1, test.view.getItemsize()); - } + announce("getItemsize"); + // Unity for all test material + assertEquals(1, view.getItemsize()); + } + + /** Test method for {@link org.python.core.PyBuffer#toString()}. */ + @Test + public void testToString() { + announce("toString"); + String r = view.toString(); + assertEquals("buffer does not match reference", ref.string, r); } /** - * Test method for {@link org.python.core.PyBuffer#toString()}. + * Custom assert method comparing the bytes at a {@link PyBuffer.Pointer} to those in a byte + * array, when that Pointer is obtained from a contiguous PyBuffer. + * Let bp[i] denote bp.storage[bp.offset+i], by analogy with a C + * pointer. It is required that bp[k] == expected[k], for every index in + * expected. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param bp result to test */ - public void testToString() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("toString: " + test); - } - String r = test.view.toString(); - assertEquals("buffer does not match reference", test.material.string, r); - } + @SuppressWarnings("deprecation") + private static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp) { + assertBytesEqual(message, expected, bp, 1); + } + + /** + * Custom assert method comparing the bytes at a {@link PyBuffer.Pointer} to those in a byte + * array, when that Pointer is obtained from a striding PyBuffer. Let + * bp[i] denote bp.storage[bp.offset+i], by analogy with a C pointer. + * It is required that bp[k*stride] == expected[k], for every index k + * in expected. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param bp result to test + * @param stride in the bp.storage array + */ + @SuppressWarnings("deprecation") + private static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp, + int stride) { + ByteBufferTestSupport.assertBytesEqual(message, expected, 0, expected.length, bp.storage, + bp.offset, stride); } /* @@ -1415,545 +1226,4 @@ } - /** - * Class to hold test material representing the same sequence of values 0..255 in several - * different ways. - */ - protected static class ByteMaterial { - - final String string; - final byte[] bytes; - final int[] ints; - final int length; - - /** Construct from String. */ - public ByteMaterial(String s) { - string = s; - length = s.length(); - bytes = new byte[length]; - ints = new int[length]; - for (int i = 0; i < length; i++) { - int x = s.charAt(i); - ints[i] = x; - bytes[i] = (byte)x; - } - } - - /** Construct from byte array. */ - public ByteMaterial(byte[] b) { - length = b.length; - StringBuilder buf = new StringBuilder(length); - bytes = new byte[length]; - ints = new int[length]; - for (int i = 0; i < length; i++) { - int x = 0xff & b[i]; - ints[i] = x; - bytes[i] = (byte)x; - buf.appendCodePoint(x); - } - string = buf.toString(); - } - - /** Construct from int array. */ - public ByteMaterial(int[] a) { - length = a.length; - StringBuilder buf = new StringBuilder(length); - bytes = new byte[length]; - ints = new int[length]; - for (int i = 0; i < length; i++) { - int x = a[i]; - ints[i] = x; - bytes[i] = (byte)x; - buf.appendCodePoint(x); - } - string = buf.toString(); - } - - /** Construct from pattern on values (used modulo 256). */ - public ByteMaterial(int start, int count, int inc) { - length = count; - StringBuilder buf = new StringBuilder(length); - bytes = new byte[length]; - ints = new int[length]; - int x = start; - for (int i = 0; i < length; i++) { - ints[i] = x; - bytes[i] = (byte)x; - buf.appendCodePoint(x); - x = (x + inc) & 0xff; - } - string = buf.toString(); - } - - @Override - public String toString() { - StringBuilder buf = new StringBuilder(100); - buf.append("byte[").append(length).append("]={ "); - for (int i = 0; i < length; i++) { - if (i > 0) { - buf.append(", "); - } - if (i >= 5) { - buf.append(" ..."); - break; - } else { - buf.append(ints[i]); - } - } - buf.append(" }"); - return buf.toString(); - } - - /** - * @return a copy of the bytes array (that the client is allowed to modify) - */ - byte[] getBytes() { - return bytes.clone(); - } - - /** - * Create material equivalent to a slice. this will not be used to create an exporter, but - * rather to specify data equivalent to the export. - * - * @param start first index to include - * @param length number of indices - * @param stride between indices - * @return ByteMaterial in which the arrays are a slice of this one - */ - ByteMaterial slice(int start, int length, int stride) { - return new ByteMaterial(sliceBytes(bytes, start, length, stride)); - } - } - - /** - * Create a byte array from the values of the PyBuffer obtained using - * {@link PyBuffer#byteAt(int)}, to a length obtained from {@link PyBuffer#getLen()}. - * - * @param v the buffer - * @return the byte array - */ - static byte[] bytesFromByteAt(PyBuffer v) { - final int N = v.getLen(); - byte[] a = new byte[N]; - for (int i = 0; i < N; i++) { - a[i] = v.byteAt(i); - } - return a; - } - - /** - * Create a byte array that is a strided copy of the one passed in. The specifications are - * assumed correct for the size of that array. - * - * @param b source array - * @param start first index to include - * @param length number of indices - * @param stride between indices - * @return slice of b - */ - static byte[] sliceBytes(byte[] b, int start, int length, int stride) { - byte[] a = new byte[length]; - for (int i = 0, j = start; i < length; i++, j += stride) { - a[i] = b[j]; - } - return a; - } - - /** - * Custom assert method comparing the bytes at a {@link PyBuffer.Pointer} to those in a byte - * array, when that Pointer is obtained from a contiguous PyBuffer. - * Let bp[i] denote bp.storage[bp.offset+i], by analogy with a C - * pointer. It is required that bp[k] == expected[k], for every index in - * expected. If not, a fail() is declared. - * - * @param message to issue on failure - * @param expected expected byte array - * @param bp result to test - */ - @SuppressWarnings("deprecation") - static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp) { - assertBytesEqual(message, expected, bp, 1); - } - - /** - * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte - * array, when that ByteBuffer is obtained from a contiguous PyBuffer. - * Let bb[i] denote bb.get(bb.position()+i), by analogy with a C - * pointer. It is required that bb[k] == expected[k], for every index - * k in expected. If not, a fail() is declared. - * - * @param message to issue on failure - * @param expected expected byte array - * @param bb result to test - */ - static void assertBytesEqual(String message, byte[] expected, ByteBuffer bb) { - // Use the position-advancing buffer get() - byte[] actual = new byte[expected.length]; - assertEquals( message + " (size in buffer)", expected.length, bb.remaining()); - bb.get(actual); - assertBytesEqual(message, expected, actual); - } - - /** - * Custom assert method comparing the bytes at a {@link PyBuffer.Pointer} to those in a byte - * array, when that Pointer is obtained from a striding PyBuffer. Let - * bp[i] denote bp.storage[bp.offset+i], by analogy with a C pointer. - * It is required that bp[k*stride] == expected[k], for every index k - * in expected. If not, a fail() is declared. - * - * @param message to issue on failure - * @param expected expected byte array - * @param bp result to test - * @param stride in the bp.storage array - */ - @SuppressWarnings("deprecation") - static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp, int stride) { - assertBytesEqual(message, expected, 0, expected.length, bp.storage, bp.offset, stride); - } - - /** - * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte - * array, when that ByteBuffer is obtained from a striding PyBuffer. - * Let bb[i] denote bb.get(bb.position()+i), by analogy with a C - * pointer. It is required that bb[k*stride] == expected[k], for every index - * k in expected. If not, a fail() is declared. - * - * @param message to issue on failure - * @param expected expected byte array - * @param bb result to test - * @param stride in the buffer bb - */ - static void assertBytesEqual(String message, byte[] expected, ByteBuffer bb, int stride) { - assertBytesEqual(message, expected, 0, expected.length, bb, stride); - } - - /** - * Custom assert method comparing the bytes at a {@link PyBuffer.Pointer} to those in a byte - * array, when that Pointer is obtained from a striding PyBuffer. Let - * bp[i] denote bp.storage[bp.offset+i], by analogy with a C pointer. - * It is required that bp[k*stride] == expected[expectedStart+k], for - * k=0 to n-1. If not, a fail() is declared. - * - * @param message to issue on failure - * @param expected expected byte array - * @param expectedStart where to start the comparison in expected - * @param n number of bytes to test - * @param bp result to test - * @param stride in the bp.storage array - */ - @SuppressWarnings("deprecation") - static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, - PyBuffer.Pointer bp, int stride) { - assertBytesEqual(message, expected, expectedStart, n, bp.storage, bp.offset, stride); - } - - /** - * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte - * array, when that ByteBuffer is obtained from a striding PyBuffer. - * Let bb[i] denote bb.get(bb.position()+i), by analogy with a C - * pointer. It is required that bb[k*stride] == expected[expectedStart+k], for - * k=0 to n-1. If not, a fail() is declared. - * - * @param message to issue on failure - * @param expected expected byte array - * @param expectedStart where to start the comparison in expected - * @param n number of bytes to test - * @param bb result to test - * @param stride in the buffer bb - */ - static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, - ByteBuffer bb, int stride) { - // Note that this approach leaves the buffer position unmodified - int p = bb.position(); - byte[] actual = new byte[n]; - for (int k = 0; k < n; k++, p += stride) { - actual[k] = bb.get(p); - } - assertBytesEqual(message, expected, expectedStart, n, actual, 0); - } - - /** - * Custom assert method comparing byte arrays: values in actual[] must match all - * those in expected[], and they must be the same length. - * - * @param message to issue on failure - * @param expected expected byte array - * @param actual result to test - */ - static void assertBytesEqual(String message, byte[] expected, byte[] actual) { - assertEquals(message + " (array size)", expected.length, actual.length); - assertBytesEqual(message, expected, 0, expected.length, actual, 0, 1); - } - - /** - * Custom assert method comparing byte arrays. It is required that - * actual[k] == expected[k], for k=0 to expected.length-1 - * . If not, a fail() is declared. - * - * @param message to issue on failure - * @param expected expected byte array - * @param actual result to test - * @param actualStart where to start the comparison in actual - */ - static void assertBytesEqual(String message, byte[] expected, byte[] actual, int actualStart) { - assertBytesEqual(message, expected, 0, expected.length, actual, actualStart, 1); - } - - /** - * Custom assert method comparing byte arrays. It is required that - * actual[actualStart+k] == expected[expectedStart+k], for k=0 to - * n-1. If not, a fail() is declared. - * - * @param message to issue on failure - * @param expected expected byte array - * @param expectedStart where to start the comparison in expected - * @param n number of bytes to test - * @param actual result to test - * @param actualStart where to start the comparison in actual - */ - static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, - byte[] actual, int actualStart) { - assertBytesEqual(message, expected, expectedStart, n, actual, actualStart, 1); - } - - /** - * Custom assert method comparing byte arrays. It is required that - * actual[actualStart+k*stride] == expected[expectedStart+k], for - * k=0 to n-1. If not, a fail() is declared. - * - * @param message to issue on failure - * @param expected expected byte array - * @param expectedStart where to start the comparison in expected - * @param n number of bytes to test - * @param actual result to test - * @param actualStart where to start the comparison in actual - * @param stride spacing of bytes in actual array - */ - static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, - byte[] actual, int actualStart, int stride) { - - if (actualStart < 0) { - fail(message + " (start<0 in result)"); - - } else if (expectedStart < 0) { - fail(message + " (start<0 in expected result): bug in test?"); - - } else if (actualStart + (n - 1) * stride + 1 > actual.length) { - fail(message + " (result too short)"); - - } else if (expectedStart + n > expected.length) { - fail(message + " (expected result too short): bug in test?"); - - } else { - // Should be safe to compare the values - int i = actualStart, j, jLimit = expectedStart + n; - for (j = expectedStart; j < jLimit; j++) { - if (actual[i] != expected[j]) { - break; - } - i += stride; - } - - // If we stopped early, diagnose the problem - if (j < jLimit) { - byte[] a = Arrays.copyOfRange(actual, actualStart, actualStart + n); - byte[] e = Arrays.copyOfRange(expected, expectedStart, expectedStart + n); - System.out.println(" expected:" + Arrays.toString(e)); - System.out.println(" actual:" + Arrays.toString(a)); - System.out.println(" _actual_:" + Arrays.toString(actual)); - fail(message + " (byte at " + j + ")"); - } - } - } - - /** - * Customised assert method comparing a int arrays: values in the actual value starting at - * actual[offset] must match all those in expected[], and there must be enough of them. - * - * @param message to issue on failure - * @param expected expected array - * @param actual result to test - * @param offset where to start the comparison in actual - */ - static void assertIntsEqual(String message, int[] expected, int[] actual, int offset) { - int n = expected.length; - if (offset < 0) { - fail(message + " (offset<0)"); - } else if (offset + n > actual.length) { - fail(message + " (too short)"); - } else { - // Should be safe to compare the values - int i = offset, j; - for (j = 0; j < n; j++) { - if (actual[i++] != expected[j]) { - break; - } - } - if (j < n) { - System.out.println(" expected:" + Arrays.toString(expected)); - System.out.println(" actual:" + Arrays.toString(actual)); - fail(message + " (int at " + j + ")"); - } - } - } - - /** - * Customised assert method comparing a int arrays: int in the actual value must match all those - * in expected[], and there must be the same number of them. - * - * @param message to issue on failure - * @param expected expected array - * @param actual result to test - */ - static void assertIntsEqual(String message, int[] expected, int[] actual) { - int n = expected.length; - assertEquals(message, n, actual.length); - // Should be safe to compare the values - int j; - for (j = 0; j < n; j++) { - if (actual[j] != expected[j]) { - break; - } - } - if (j < n) { - System.out.println(" expected:" + Arrays.toString(expected)); - System.out.println(" actual:" + Arrays.toString(actual)); - fail(message + " (int at " + j + ")"); - } - } - - /** - * Within a given test case (e.g. the test of one particular method) we run many data sets, and - * these are created by {@link PyBufferTest#setUp()} as instances of this class. The main - * contents of the BufferTestPair are the test subject and the material. The subject may be one - * of the base objects specified in 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. - */ - 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}; - - /** The shape array that the subject should match (will be single element in present tests) */ - int[] shape; - - /** The shape array that the subject should match (will be single element in present tests) */ - int[] strides; - - /** - * A subject and its reference material, together with explicit shape and strides arrays - * expected. - * - * @param subject of the test - * @param material containing a Java byte array that a view of the subject should equal - * @param shape of the array, when testing in N-dimensions - * @param strides of the array, when testing sliced views - * @param readonly if true the base exporter can only provide read-only views - */ - public BufferTestPair(BufferProtocol subject, ByteMaterial material, int[] shape, - int[] strides, boolean readonly, int[] validFlags, int[] validTassles) { - this.subject = subject; - this.material = new ByteMaterial(material.ints); // Copy in case modified - this.shape = shape; - this.strides = strides; - this.readonly = readonly; - this.validFlags = validFlags; - this.validTassles = validTassles; - - int flags = readonly ? PyBUF.FULL_RO : PyBUF.FULL; - - if (subject instanceof PyBuffer) { - this.view = (PyBuffer)subject; - } else { - PyBuffer v = null; - try { - // System.out.printf("BufferTestPair: length=%d, readonly=%s\n", - // material.length, readonly); - v = subject.getBuffer(flags); - } catch (Exception e) { - /* - * We ignore this case if we fail, because we are not testing buffer creation - * here, but making buffers to be tested. We'll test buffer creation in - * testGetBuffer. - */ - } - this.view = v; - } - } - - /** - * Short constructor for contiguous arrays in one dimension. - * - * @param subject of the test - * @param material containing a Java byte array that a view of the subject should equal - * @param readonly if true the base exporter can only provide read-only views - */ - public BufferTestPair(BufferProtocol subject, ByteMaterial material, boolean readonly) { - this(subject, material, new int[1], STRIDES_1D, readonly, simpleFlags, simpleTassles); - shape[0] = material.length; - } - - /** - * Short constructor for strided arrays in one dimension. - * - * @param subject of the test - * @param material containing a Java byte array that a view of the subject should equal - * @param stride of the array, when testing sliced views - * @param readonly if true the base exporter can only provide read-only views - */ - public BufferTestPair(PyBuffer subject, ByteMaterial material, int stride, boolean readonly) { - this(subject, material, new int[1], new int[1], readonly, strided1DFlags, - strided1DTassles); - shape[0] = material.length; - strides[0] = stride; - } - - @Override - public String toString() { - int offset0 = view.getBuf().offset; // XXX -> view.getNIOByteBuffer().position() ? - int offset = view.getNIOByteBuffer().position(); - assertEquals(offset0, 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() - + " )"; - } - - } } diff --git a/tests/java/org/python/core/PyBufferTestSupport.java b/tests/java/org/python/core/PyBufferTestSupport.java new file mode 100644 --- /dev/null +++ b/tests/java/org/python/core/PyBufferTestSupport.java @@ -0,0 +1,543 @@ +package org.python.core; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import org.python.core.ByteBufferTestSupport.ByteMaterial; + +/** + * Supporting test fixtures for testing {@link PyBuffer} implementations, this class provides means + * to generate test specifications and organise them into a list. This object creates and holds + * factories for the multiple examples of the several implementation types the PyBufferTest needs, + * together with the configuration the factory and the test need. + */ +public class PyBufferTestSupport { + + /** Control amount of output while generating material. */ + protected int verbosity; + + /** Lengths we will use if we can when slicing view */ + private final int[] sliceLengths; + + /** Step sizes we will use if we can when slicing view */ + private final int[] sliceSteps; + + /** List of test data configurations. */ + private List testSpecList = new LinkedList(); + + /** + * Create an instance, and choose the number an variety of tests that each call to + * {@link #generate(BufferProtocol, ByteMaterial, boolean)} will produce. + * + * @param sliceLengths what length of slices to try to make from each original + * @param sliceSteps step sizes (strides) to try to use + */ + PyBufferTestSupport(int[] sliceLengths, int[] sliceSteps) { + this(0, sliceLengths, sliceSteps); + } + + /** + * Create an instance, and choose the number an variety of tests that each call to + * {@link #generate(BufferProtocol, ByteMaterial, boolean)} will produce. + * + * @param verbosity how much noise to make when generating test data + * @param sliceLengths what length of slices to try to make from each original + * @param sliceSteps step sizes (strides) to try to use + */ + PyBufferTestSupport(int verbosity, int[] sliceLengths, int[] sliceSteps) { + this.verbosity = verbosity; + this.sliceLengths = sliceLengths; + this.sliceSteps = sliceSteps; + } + + /** + * Add to the test queue a series of test specifications for a particular type of exporter and + * byte material, in various sliced versions. The first argument provides a factory able to + * construct a test object bearing the {@link BufferProtocol} interface, from the + * {@link ByteMaterial} also supplied. The first test specification queued is based directly on + * such construction. Construction takes place when {@link TestSpec#make()} is called during the + * test constructor. + *

+ * The method goes on to create a series of specifications that when invoked in test + * initialisation will provide sliced views. + *

+ * When the test runs, it will be given one test specification. Either: + *

    + *
  1. the test is given the original root specification and makes a PyBuffer from + * it, by a call to {@link TestSpec#make()}, whose implementation creates a test subject of + * appropriate type, or
  2. + *
  3. the test is given a derived sliced specification and makes a buffer from it, by a call to + * {@link TestSpec#make()}, whose implementation slices a buffer provided by the original root + * specification.
  4. + *
+ * The slices are made with a variety of argument combinations, filtered down to those that make + * sense for the size of the direct view. The reference value in the derived specification + * {@link TestSpec#ref} is computed independently of the test subject, from the slice + * specification and a the reference value in the root specification. + * + * @param original to specify a test and from which to generate other tests + */ + void add(ExporterFactory factory, ByteMaterial material) { + + // Add test using the specification passed as arguments + TestSpec original = new TestSpec(factory, material); + queue(original); + + // Generate some slices from the material and this direct view + int N = original.ref.length; + int M = (N + 4) / 4; // At least one and about N/4 + + // For a range of start positions up to one beyond the end + for (int start = 0; start <= N; start += M) { + // For a range of lengths + for (int length : sliceLengths) { + + if (length == 0) { + queue(original, start, 0, 1); + queue(original, start, 0, 2); + + } else if (length == 1 && start < N) { + queue(original, start, 1, 1); + queue(original, start, 1, 2); + + } else if (start < N) { + + // And for a range of step sizes + for (int step : sliceSteps) { + // Check this is a feasible slice + if (start + (length - 1) * step < N) { + queue(original, start, length, step); + } + } + + // Now use all the step sizes negatively + for (int step : sliceSteps) { + // Check this is a feasible slice + if (start - (length - 1) * step >= 0) { + queue(original, start, length, -step); + } + } + } + } + } + } + + /** Generate and queue one test of non-slice type (if getting a buffer succeeds). */ + private void queue(TestSpec spec) { + if (verbosity > 2) { + System.out.printf("queue non-slice: length=%d, readonly=%s\n", spec.ref.length, + spec.readonly); + } + testSpecList.add(spec); + } + + /** Generate and queue one test of slice type (if getting a buffer succeeds). */ + private void queue(TestSpec original, int start, int length, int step) { + /* + * Make a slice. We ignore this case if we fail, because we are not testing slice creation + * here, but making slices to be tested as buffers. We'll test slice creation in + * testGetBufferSlice. + */ + try { + if (verbosity > 2) { + System.out.printf(" queue slice: start=%4d, length=%4d, step=%4d\n", start, + length, step); + } + TestSpec spec = new SlicedTestSpec(original, 1, start, length, step); + testSpecList.add(spec); + } catch (Exception e) { + /* + * We ignore this case if we fail, because we are not testing slice creation here, but + * making slices to be tested as buffers. We'll test slice creation elsewhere. + */ + if (verbosity > 2) { + System.out.printf("*** SKIP %s\n", e); + } + } + } + + /** + * Return a copy of the generated list of test data in a form suitable for test construction + * with a JUnit parameterised runner, which is as a collection of arrays of objects, where each + * array becomes the arguments to the test constructor. (@see org.junit.runners.Parameterized) + * + * @return generated list of test data + */ + List getTestData() { + List r = new ArrayList(testSpecList.size()); + for (TestSpec spec : testSpecList) { + r.add(new TestSpec[] {spec}); + } + return r; + } + + /** + * Create a byte array from the values of the PyBuffer obtained using + * {@link PyBuffer#byteAt(int)}, to a length obtained from {@link PyBuffer#getLen()}. + * + * @param v the buffer + * @return the byte array + */ + static byte[] bytesFromByteAt(PyBuffer v) { + final int N = v.getLen(); + byte[] a = new byte[N]; + for (int i = 0; i < N; i++) { + a[i] = v.byteAt(i); + } + return a; + } + + /** + * Interface to a factory capable of making a {@link PyBuffer} exporter from + * {@link ByteMaterial}. + */ + interface ExporterFactory { + + /** Make fresh test object. */ + BufferProtocol make(ByteMaterial m); + + /** Whether the test object will be read-only. */ + boolean isReadonly(); + + /** Whether the test object will be able to provide access as a byte array. */ + boolean hasArray(); + } + + abstract static class ReadonlyExporterFactory implements ExporterFactory { + + @Override + public boolean isReadonly() { + return true; + } + + @Override + public boolean hasArray() { + return true; + } + + }; + + abstract static class WritableExporterFactory implements ExporterFactory { + + @Override + public boolean isReadonly() { + return false; + } + + @Override + public boolean hasArray() { + return true; + } + + }; + + /** + * Class holding reference data for a test and a factory method that will produce an object with + * interface {@link BufferProtocol} for use in tests. The class has one principal method + * {@link TestSpec#makePair()}, which must return an {@link ObjectAndView} where the view + * element is equal to the reference {@link TestSpec#ref}. During a JUnit test, the test + * constructor will be called with a particular instance of this class and will call + * makePair() one or more times to get fresh test material. + */ + static class TestSpec { + + /** Factory for test objects. */ + final ExporterFactory factory; + /** The value of the associated test object. */ + final ByteMaterial ref; + /** The associated PyBuffer should be read-only. */ + final boolean readonly; + /** The associated PyBuffer should be accessible as a JVM array. */ + final boolean hasArray; + /** Parent TestSpec, when this is a derived one, or null if it is an original. */ + final TestSpec parent; + /** The value of shape array that the view should have that matches {@link #ref}. */ + final int[] shape; + /** The value of strides array that the view should have that matches {@link #ref}. */ + final int[] strides; + + /** Either {@link PyBUF#FULL_RO} or {@link PyBUF#FULL} according to {@link #readonly}. */ + final int flags; + + /** Allowable basic flag combinations, such as */ + final int[] validFlags; + + /** Allowable additional flag combinations, such as {@link PyBUF#FORMAT} */ + final int[] validTassles; + + /** + * A one-dimensional exporter should be able to give us a buffer for all these flag types. + */ + static final int[] simpleFlags = {PyBUF.SIMPLE, PyBUF.ND, PyBUF.STRIDES, PyBUF.INDIRECT, + PyBUF.FULL_RO}; + + /** To {@link #simpleFlags} we can add any of these */ + static final int[] simpleTassles = {0, PyBUF.FORMAT, PyBUF.C_CONTIGUOUS, + PyBUF.F_CONTIGUOUS, PyBUF.ANY_CONTIGUOUS}; + + /** + * Construct a specification for a 1D contiguous byte-array based on the exporter factory + * and reference data supplied. + * + * @param factory makes exporters of the particular type + * @param ref the fill those exporters should have + */ + TestSpec(ExporterFactory factory, ByteMaterial ref) { + this(null, factory, ref, new int[] {ref.length}, new int[] {1}, simpleFlags, + simpleTassles); + } + + /** + * Construct a specification for a 1D contiguous item-array based on the exporter factory, + * shape data and reference data supplied. + * + * @param parent of this test specification + * @param ref the fill those exporters should have (also determines the item size) + * @param shape array defining number and size of dimensions (as {@link PyBUF#getShape()} + * @param strides array defining addressing polynomial (as {@link PyBUF#getStrides()}) + * @param validFlags allowable basic flag combinations usable with this specification + * @param validTassles allowable additional flag combinations + */ + protected TestSpec(TestSpec parent, ByteMaterial ref, int[] shape, int[] strides, + int[] validFlags, int[] validTassles) { + this(parent, parent.getOriginal().factory, ref, shape, strides, validFlags, + validTassles); + } + + /** + * Construct a specification for a 1D contiguous item-array based on the exporter factory, + * shape data and reference data supplied. + * + * @param parent of this test specification + * @param factory makes exporters of the particular type, given ref + * @param ref the fill those exporters should have (also determines the item size) + * @param shape array defining number and size of dimensions (as {@link PyBUF#getShape()} + * @param strides array defining addressing polynomial (as {@link PyBUF#getStrides()}) + * @param validFlags allowable basic flag combinations usable with this specification + * @param validTassles allowable additional flag combinations + */ + protected TestSpec(TestSpec parent, ExporterFactory factory, ByteMaterial ref, int[] shape, + int[] strides, int[] validFlags, int[] validTassles) { + this.parent = parent; + this.factory = factory; + this.readonly = factory.isReadonly(); + this.hasArray = factory.hasArray(); + this.flags = (readonly ? PyBUF.FULL_RO : PyBUF.FULL) | (hasArray ? PyBUF.AS_ARRAY : 0); + this.ref = ref; + this.shape = shape; + this.strides = strides; + this.validFlags = validFlags; + this.validTassles = validTassles; + } + + /** Return the parent of this specification (or null when it is an original). */ + final TestSpec getParent() { + return parent; + } + + /** This is an original specification (parent is null). */ + final boolean isOriginal() { + return parent == null; + } + + /** Return the original of this specification (ancestor with no parent). */ + final TestSpec getOriginal() { + TestSpec p = this; + while (!p.isOriginal()) { + p = p.getParent(); + } + return p; + } + + /** Return the item size. */ + int getItemsize() { + return 1; + } + + /** Return the stride that a buffer made from this specification should have. */ + int getStride() { + return strides[0]; + } + + /** Return the start index that a buffer made from this specification should have. */ + int getStart() { + return 0; + } + + /** Simple holder class for a buffer exporter object and a related buffer. */ + static class ObjectAndView { + + final BufferProtocol obj; + final PyBuffer view; + + ObjectAndView(BufferProtocol obj, PyBuffer view) { + this.obj = obj; + this.view = view; + } + } + + /** + * Make the test object which must implement BufferProtocol and its + * PyBuffer view. The value as a byte array must equal {@link #ref}. + */ + public ObjectAndView makePair() { + BufferProtocol obj = factory.make(ref); + PyBuffer view = obj.getBuffer(flags); + return new ObjectAndView(obj, view); + } + + @SuppressWarnings("deprecation") + @Override + public String toString() { + + ObjectAndView pair = makePair(); + BufferProtocol obj = pair.obj; + PyBuffer view = pair.view; + + StringBuilder sb = new StringBuilder(100); + sb.append(obj.getClass().getSimpleName()).append('['); + + int offset, stride = getStride(); + + if (view.hasArray()) { + offset = view.getBuf().offset; + } else { + offset = view.getNIOByteBuffer().position(); + } + + if (offset > 0) { + sb.append(offset); + } + + String plus = offset == 0 ? "" : "+"; + + if (stride == 1) { + sb.append(plus).append("k]"); + } else if (stride == -1) { + sb.append("-k]"); + } else if (stride < 0) { + sb.append("-").append(-stride).append("*k]"); + } else { + /* stride>1 or ==0) */sb.append(plus).append(stride).append("*k]"); + } + + while (sb.length() < 30) { + sb.append(' '); + } + sb.append("ref = ").append(ref.toString()); + + return sb.toString(); + } + } + + /** + * A test specification that is derived from a parent test specification, but will construct + * views sliced a particular way. In order to construct a test object, the factory of the parent + * is used, so that objects returned from here have the same type and root buffer value as the + * parent. However, {@link SlicedTestSpec#makePair()} returns a sliced view with the base + * exporter, and the reference material here is sliced correspondingly. + */ + static class SlicedTestSpec extends TestSpec { + + /** Number of consecutive bytes forming one item */ + final int itemsize; + /** Index in the parent object of item 0 of this slice */ + final int first; + /** The number of items that make up the slice. */ + final int count; + /** The item-index distance in the parent from one item to the next of this slice. */ + final int step; + + /** Byte-index in the original byte-array object of byte 0 of item 0 of the slice */ + final int start; + + /** + * A one-dimensional exporter with stride!=1 is restricted to give us a buffer only for + * these flag types. + */ + static final int[] strided1DFlags = {PyBUF.STRIDES, PyBUF.INDIRECT, PyBUF.FULL_RO}; + + /** To {@link #strided1DFlags} we can add any of these */ + static final int[] strided1DTassles = {0, PyBUF.FORMAT}; + + /** + * Construct a test specification based on a parent, but yielding objects and reference + * material whose values are related to those of the parent according to the slice + * specification. + * + * @param parent specification of byte buffer to slice + * @param size number of consecutive bytes forming one item + * @param first byte-index in the parent of byte 0 of item 0 the result + * @param count number of items in the slice + * @param step byte-index increment in the parent between items + */ + SlicedTestSpec(TestSpec parent, int size, int first, int count, int step) { + super(parent, parent.ref.slice(size, first, count, step), new int[] {count}, + new int[1], strided1DFlags, strided1DTassles); + // It only seems to make sense for byte-array parent (or does all scale?) + if (parent.getItemsize() != 1) { + throw new IllegalArgumentException("Only byte-array parent supported"); + } + this.itemsize = size; + // Write these down verbatim for subsequent call to getBufferSlice + this.first = first; + this.count = count; + this.step = step; + // But these must be calculated carefully + this.start = parent.getStart() + first * parent.getStride(); + this.strides[0] = step * parent.getStride(); + } + + /** + * {@inheritDoc} + *

+ * The size given in construction of a SlicedTestSpec. + */ + @Override + int getItemsize() { + return itemsize; + } + + /** + * {@inheritDoc} + *

+ * The start given in construction of a SlicedTestSpec is a start byte index + * specification, which could itself be striding on the underlying object's storage. + */ + @Override + int getStart() { + return start; + } + + /** + * {@inheritDoc} + *

+ * In SlicedTestSpec the returned pair are a new instance of the root object + * (to be the original exporter) created by + * + *

+         * pair = parent.makePair();
+         * obj = pair.obj;
+         * 
+ * and a sliced buffer view onto it created by + * + *
+         * view = pair.view.getBufferSlice(flags, first, count, step);
+         * 
+ * This view-slicing will apply recursively if the parent is a {@link SlicedTestSpec}, just + * as the slicing of reference material was iterated in construction. + */ + @Override + public ObjectAndView makePair() { + // Create a fresh test object and buffer view from the parent spec + ObjectAndView pair = parent.makePair(); + // Make a sliced view and release the parent + PyBuffer view = pair.view.getBufferSlice(flags, first, count, step); + // Controlled release of the parent buffer since pair is local + pair.view.release(); + return new ObjectAndView(pair.obj, view); + } + + } +} -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:24 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:24 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Fix_mis-handling_of_the_AS?= =?utf-8?q?=5FARRAY_flag_in_BaseBuffer=2EhasArray?= Message-ID: <20160827131223.67844.73763.5B152159@psf.io> https://hg.python.org/jython/rev/2aee2b3d18bf changeset: 7939:2aee2b3d18bf user: Jeff Allen date: Tue May 31 17:53:24 2016 +0100 summary: Fix mis-handling of the AS_ARRAY flag in BaseBuffer.hasArray PyBufferTest will now run cleanly. files: src/org/python/core/buffer/BaseBuffer.java | 4 ++-- 1 files changed, 2 insertions(+), 2 deletions(-) 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 @@ -212,13 +212,13 @@ @Override public boolean isReadonly() { // WRITABLE is a non-navigational flag, so is inverted in gFeatureFlags - return (gFeatureFlags & WRITABLE) != 0; + return (gFeatureFlags & WRITABLE) != 0; // i.e. featureFlags & WRITABLE is false } @Override public boolean hasArray() { // AS_ARRAY is a non-navigational flag, so is inverted in gFeatureFlags - return (gFeatureFlags & AS_ARRAY) != 0; + return (gFeatureFlags & AS_ARRAY) == 0; // i.e. featureFlags & AS_ARRAY is true } @Override -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:24 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:24 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Prevent_support_files_being?= =?utf-8?q?_treated_as_Java_tests=2E?= Message-ID: <20160827131224.24423.27633.42E9A4BC@psf.io> https://hg.python.org/jython/rev/e6e0fede9aac changeset: 7940:e6e0fede9aac user: Jeff Allen date: Tue May 31 18:51:32 2016 +0100 summary: Prevent support files being treated as Java tests. files: build.xml | 4 ++-- 1 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.xml b/build.xml --- a/build.xml +++ b/build.xml @@ -925,7 +925,7 @@ - + @@ -947,7 +947,7 @@ - + -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:25 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:25 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Implementation_of_PyBuffer?= =?utf-8?q?=2EcopyFrom_dealing_correctly_with_overlap=2E?= Message-ID: <20160827131225.39049.61877.5C11B82F@psf.io> https://hg.python.org/jython/rev/ac7255b9f994 changeset: 7945:ac7255b9f994 user: Jeff Allen date: Sun Jun 26 22:04:50 2016 +0100 summary: Implementation of PyBuffer.copyFrom dealing correctly with overlap. Fixes regression in test_memoryview due to recent refactoring. JUnit test provided explicitly for copyFrom() sliced versions of self. files: src/org/python/core/buffer/Base1DBuffer.java | 2 +- src/org/python/core/buffer/BaseArrayBuffer.java | 72 ++-- src/org/python/core/buffer/BaseBuffer.java | 17 +- src/org/python/core/buffer/BaseNIOBuffer.java | 55 --- tests/java/org/python/core/PyBufferTest.java | 161 ++++++++- 5 files changed, 180 insertions(+), 127 deletions(-) diff --git a/src/org/python/core/buffer/Base1DBuffer.java b/src/org/python/core/buffer/Base1DBuffer.java --- a/src/org/python/core/buffer/Base1DBuffer.java +++ b/src/org/python/core/buffer/Base1DBuffer.java @@ -77,7 +77,7 @@ } else if (stride > 0) { return index0 + (shape[0] - 1) * stride; } else { - return index0 - 1; + return index0; } } diff --git a/src/org/python/core/buffer/BaseArrayBuffer.java b/src/org/python/core/buffer/BaseArrayBuffer.java --- a/src/org/python/core/buffer/BaseArrayBuffer.java +++ b/src/org/python/core/buffer/BaseArrayBuffer.java @@ -113,6 +113,23 @@ @Override public void copyFrom(byte[] src, int srcPos, int destIndex, int count) throws IndexOutOfBoundsException, PyException { + copyFrom(src, srcPos, 1, destIndex, count); + } + + /** + * Generalisation of {@link PyBuffer#copyFrom(byte[], int, int, int)} to allow a stride within + * the source array. + * + * @param src source byte array + * @param srcPos byte-index location in source of first byte to copy + * @param srcStride byte-index increment from one item to the next + * @param destIndex starting item-index in the destination (i.e. this) + * @param count number of items to copy in + * @throws IndexOutOfBoundsException if access out of bounds in source or destination + * @throws PyException (TypeError) if read-only buffer + */ + protected void copyFrom(byte[] src, int srcPos, int srcStride, int destIndex, int count) + throws IndexOutOfBoundsException, PyException { checkWritable(); @@ -123,16 +140,19 @@ int skip = stride - itemsize; int d = byteIndex(destIndex); + int srcSkip = srcStride - itemsize; + // Strategy depends on whether items are laid end-to-end or there are gaps - if (skip == 0) { + if (skip == 0 && srcSkip == 0) { // Straight copy of contiguous bytes System.arraycopy(src, srcPos, storage, d, count * itemsize); } else { - // Non-contiguous copy: single byte items int limit = d + count * stride, s = srcPos; if (itemsize == 1) { + // Non-contiguous copy: single byte items for (; d != limit; d += stride) { - storage[d] = src[s++]; + storage[d] = src[s]; + s += srcStride; } } else { // Non-contiguous copy: itemsize bytes then skip to next item @@ -141,6 +161,7 @@ while (d < t) { storage[d++] = src[s++]; } + s += srcSkip; } } } @@ -149,56 +170,41 @@ @Override public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { - if (src instanceof BaseArrayBuffer) { + if (src instanceof BaseArrayBuffer && !this.overlaps((BaseArrayBuffer)src)) { + // We can do this efficiently, copying between arrays. copyFromArrayBuffer((BaseArrayBuffer)src); } else { super.copyFrom(src); } } + private boolean overlaps(BaseArrayBuffer src) { + if (src.storage != this.storage) { + return false; + } else { + int low = calcLeastIndex(), high = calcGreatestIndex(); + int srcLow = src.calcLeastIndex(), srcHigh = src.calcGreatestIndex(); + return (srcHigh >= low && high >= srcLow); + } + } + private void copyFromArrayBuffer(BaseArrayBuffer src) throws IndexOutOfBoundsException, PyException { - checkWritable(); src.checkDimension(1); int itemsize = getItemsize(); int count = getSize(); - // Block operation if different item or overall size (permit reshape) + // Block operation if different item or overall size if (src.getItemsize() != itemsize || src.getSize() != count) { throw differentStructure(); } - for (int i = 0; i < count; i++) { - int s = src.byteIndex(i), d = byteIndex(i); - for (int j = 0; j < itemsize; j++) { - storage[d++] = src.byteAtImpl(s++); - } - } + // We depend on the striding copyFrom() acting directly on the source storage + copyFrom(src.storage, src.index0, src.strides[0], 0, count); } - /** - * Copy blocks of bytes, equally spaced in the source array, to locations equally spaced in the - * destination array, which may be the same array. The byte at - * src[srcPos+k*srcStride+j] will be copied to - * dst[dstPos+k*dstStride+j] for 0≤k<count and - * 0≤j<size. When the source and destination are the same array, the method - * deals correctly with the risk that a byte gets written under the alias dst[x] - * before it should have been copied referenced as src[y]. - * - * @param size of the blocks of bytes - * @param src the source array - * @param srcPos the position of the first block in the source - * @param srcStride the interval between the start of each block in the source - * @param dst the destination array - * @param dstPos the position of the first block in the destination - * @param dstStride the interval between the start of each block in the destination - * @param count the number of blocks to copy - */ - private static void slicedArrayCopy(int size, byte[] src, int srcPos, int srcStride, - byte[] dst, int dstPos, int dstStride, int count) {} - @Override protected ByteBuffer getNIOByteBufferImpl() { // The buffer spans the whole storage, which may include data not in the view 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 @@ -536,18 +536,21 @@ int itemsize = getItemsize(); int count = getSize(); + int byteLen = src.getLen(); // Block operation if different item or overall size (permit reshape) - if (src.getItemsize() != itemsize || src.getLen() != count * itemsize) { + if (src.getItemsize() != itemsize || byteLen != count * itemsize) { throw differentStructure(); } - // XXX Re-think this using ByteBuffer when the API provides a byteIndex() method - assert itemsize == 1; - // XXX This only moves the first byte of each item - for (int i = 0; i < count; i++) { - storeAt(src.byteAt(i), i); - } + /* + * It is not possible in general to know that this and src do not share storage. There is + * always a risk of incorrect results if we do not go via an intermediate byte array. + * Sub-classes may be able to avoid this. + */ + byte[] t = new byte[byteLen]; + src.copyTo(t, 0); + this.copyFrom(t, 0, 0, count); } @Override diff --git a/src/org/python/core/buffer/BaseNIOBuffer.java b/src/org/python/core/buffer/BaseNIOBuffer.java --- a/src/org/python/core/buffer/BaseNIOBuffer.java +++ b/src/org/python/core/buffer/BaseNIOBuffer.java @@ -221,61 +221,6 @@ } } - /** - * {@inheritDoc} - *

- * The default implementation in BaseNIOBuffer deals with the general - * one-dimensional case. - */ - @Override - public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { - - int length = getLen(); - int itemsize = getItemsize(); - - // Valid operation only if writable and same length and itemsize - checkWritable(); - if (src.getLen() != length || src.getItemsize() != itemsize) { - throw differentStructure(); - } - - if (length > 0) { - // Pick up attributes necessary to choose an efficient copy strategy - int stride = getStrides()[0]; - int skip = stride - itemsize; - - ByteBuffer dst = getNIOByteBuffer(); - - // Strategy depends on whether destination items are laid end-to-end or there are gaps - if (skip == 0) { - // Straight copy to contiguous bytes - for (int i = 0; i < length; i++) { - dst.put(src.byteAt(i)); - } - - } else if (itemsize == 1) { - // Non-contiguous copy: single byte items - int pos = dst.position(); - for (int i = 0; i < length; i++) { - dst.put(pos, src.byteAt(i)); - pos += stride; - } - - } else { - // Non-contiguous copy: each time, and itemsize > 1 - int pos = dst.position(); - int s = 0; - for (int i = 0; i < length; i++) { - for (int j = 0; j < itemsize; j++) { - dst.put(pos++, src.byteAt(s++)); - } - pos += skip; - } - } - } - - } - @Override protected ByteBuffer getNIOByteBufferImpl() { return storage.duplicate(); 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 @@ -556,38 +556,37 @@ } } - /** Test method for {@link org.python.core.PyBuffer#copyFrom(byte[], int, int, int)}. */ + /** Test method for {@link org.python.core.PyBuffer#copyFrom(PyBuffer)}. */ @Test public void testCopyFromPyBuffer() { - announce("copyFrom"); + announce("copyFrom (PyBuffer)"); /* * The test material (this time) presents a view that has n items of size i, that are spaced * in the underlying buffer with stride s. */ - int n = spec.ref.length; - int s = spec.getStride(); - int i = spec.getItemsize(); // = 1 in all test cases at time of writing + final int n = spec.ref.length; + final int p = spec.getStride(); // The material we copy to it should have these strides: int[] srcStrides; if (n < 2) { - srcStrides = new int[] {i}; - } else if (s > 2 * i || s < -2 * i) { - srcStrides = new int[] {i, s - i, s, s + i, -s + i, -s, -s - i}; - } else if (s == 2 * i || s == -2 * i) { - srcStrides = new int[] {i, 2 * i, 3 * i, -i, -2 * i, -3 * i}; - } else { // ( s==i || s==-i ) - srcStrides = new int[] {i, 2 * i, -i, -2 * i}; + srcStrides = new int[] {1}; + } else if (p > 2 || p < -2) { + srcStrides = new int[] {1, p - 1, p, p + 1, -p + 1, -p, -p - 1}; + } else if (p == 2 || p == -2) { + srcStrides = new int[] {1, 2, 3, -1, -2, -3}; + } else { // ( s==1 || s==-1 ) + srcStrides = new int[] {1, 2, -1, -2}; } // Also need the maximum absolute value int maxStride = 0; - for (int t : srcStrides) { - if (t > maxStride) { - maxStride = t; - } else if (-t > maxStride) { - maxStride = -t; + for (int stride : srcStrides) { + if (stride > maxStride) { + maxStride = stride; + } else if (-stride > maxStride) { + maxStride = -stride; } } @@ -597,7 +596,7 @@ // Make the source material to copy from, big enough to accommodate n strides int srcMaterialSize = n * maxStride + maxOffset; - ByteMaterial srcMaterial = new ByteMaterial(48, srcMaterialSize, i); + ByteMaterial srcMaterial = new ByteMaterial(48, srcMaterialSize, 1); /* * Now we need a series of PyBuffer views on the source data, sliced and offset according to @@ -608,21 +607,18 @@ ExporterFactory[] factories = {spec.factory, new SimpleExporterFactory()}; for (ExporterFactory factory : factories) { - /* * We'll use the same apparatus to create the source buffer as we use to make the test * cases. The specifications for them will all be derived from this one: */ TestSpec original = new TestSpec(factory, srcMaterial); - /* * Do this where the pattern of indices constituting src overlaps (or not) the pattern * of view in challenging ways, including greater and smaller strides. */ - for (int stride : srcStrides) { for (int offset : srcOffsets) { - int start = (stride > 0) ? offset : srcMaterialSize - offset - i; + int start = (stride > 0) ? offset : srcMaterialSize - offset - 1; doTestCopyFrom(original, start, n, stride); } } @@ -630,12 +626,7 @@ } - // XXX Separately test where src is a view on is the same object. - - - - /** Helper function for {@link #testCopyFromPyBuffer()} - */ + /** Helper function for {@link #testCopyFromPyBuffer()} */ private void doTestCopyFrom(TestSpec original, int start, int n, int stride) { // Derive sliced test material from the original @@ -649,9 +640,11 @@ int p = spec.getStride(); String srcName = pair.obj.getClass().getSimpleName(); if (verbosity > 1) { - System.out.printf(" copy src[%d:%d:%d] %s(%d) to obj[%d:%d:%d]\n", - start, start+n*stride, stride, srcName, - n, s, s + n * p, p); + int end = start + (n - 1) * stride + (stride > 0 ? 1 : -1); + int e = s + (n - 1) * p + (p > 0 ? 1 : -1); + System.out.printf(" copy from src[%d:%d:%d] %s(%d) to obj[%d:%d:%d]\n", // + start, end, stride, srcName, n, // + s, e, p); } // Initialise the destination object and view (have to do each time) from spec @@ -684,6 +677,112 @@ } } + /** Test method for {@link org.python.core.PyBuffer#copyFrom(PyBuffer)} when source is same. */ + @Test + public void testCopyFromSelf() { + announce("copyFrom (self)"); + + // The test material (this time) presents a view of n bytes from a buffer of L bytes. + final int n = ref.length; + TestSpec original = spec.getOriginal(); + if (spec.readonly || spec == original || n < 1) { + // We're only testing with sliced writable views + return; + } + final int p = spec.getStride(); + final int L = original.ref.length; + + /* + * We want to make another sliced view on the same test object, with the same number of + * items n, but different stride and/or offset. Strides above, equal to and below (if + * possible) the destination stride are of interest. + */ + int[] srcStrides; + if (n < 2) { + srcStrides = new int[] {1}; + } else if (p > 2 || p < -2) { + srcStrides = new int[] {1, p - 1, p, p + 1, -p + 1, -p, -p - 1}; + } else if (p == 2 || p == -2) { + srcStrides = new int[] {1, 2, 3, -1, -2, -3}; + } else { // ( p==1 || p==-1 ) + srcStrides = new int[] {1, 2, -1, -2}; + } + + for (int srcStride : srcStrides) { + int absStride; + if (srcStride > 0) { + absStride = srcStride; + /* + * Compute the highest start index such that we can fit n items spaced at absStride + * into the buffer before reaching the end. + */ + int maxOffset = L - 1 - absStride * (n - 1); + // There might not be such an start. If there is, we can do one or more tests. + if (maxOffset >= 0) { + // A positive-stepping slice could fit, for some start positions + int incOffset = 1 + maxOffset / 4; + for (int srcOffset = 0; srcOffset <= maxOffset; srcOffset += incOffset) { + doTestCopyFromSelf(srcOffset, srcStride, n); + } + } + } else {// srcStride < 0 + absStride = -srcStride; + /* + * Compute the lowest start index such that we can fit n items spaced at absStride + * into the buffer before reaching the beginning. + */ + int minOffset = absStride * (n - 1) + 1; + // There might not be such an start. If there is, we can do one or more tests. + if (minOffset < L) { + // A negative-stepping slice could fit, for some start positions + int incOffset = 1 + (L - 1 - minOffset) / 4; + for (int srcOffset = L - 1; srcOffset > minOffset; srcOffset -= incOffset) { + doTestCopyFromSelf(srcOffset, srcStride, n); + } + } + } + } + } + + /** Helper function for {@link #testCopyFromPyBuffer()} */ + private void doTestCopyFromSelf(int srcStart, int srcStride, int n) { + + // Initialise the destination object and view (have to do each time) from spec + createObjAndView(); + + // Report the slice of the test object we are writing + int dstStart = spec.getStart(); + int dstStride = spec.getStride(); + String srcName = obj.getClass().getSimpleName(); + if (verbosity > 1) { + int srcEnd = srcStart + (n - 1) * srcStride + (srcStride > 0 ? 1 : -1); + int dstEnd = dstStart + (n - 1) * dstStride + (dstStride > 0 ? 1 : -1); + System.out.printf(" copy from obj[%d:%d:%d] %s(%d) to obj[%d:%d:%d]\n", // + srcStart, srcEnd, srcStride, srcName, n, // + dstStart, dstEnd, dstStride); + } + assert !spec.readonly; // Test is only called if writable + + // Our test is against the underlying object of which the view may be a slice + try (PyBuffer underlying = obj.getBuffer(PyBUF.FULL_RO)) { + + // Take a snapshot before the call + byte[] before = bytesFromByteAt(underlying); + + // Take the required slice-view to use as the source. + PyBuffer src = underlying.getBufferSlice(PyBUF.FULL_RO, srcStart, n, srcStride); + byte[] srcBytes = bytesFromByteAt(src); + + // This is the call we are testing (a write operation). + view.copyFrom(src); + + // Test that the corresponding bytes of the underlying object match data copied in + byte[] after = bytesFromByteAt(underlying); + ByteBufferTestSupport.checkWriteCorrect(before, after, srcBytes, 0, n, 1, dstStart, + dstStride); + } + } + /** * Test method for {@link org.python.core.BufferProtocol#getBuffer()} and * {@link org.python.core.PyBuffer#getBuffer()}. -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:24 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:24 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Refactor_array-based_PyBuff?= =?utf-8?q?er_implementations=2E?= Message-ID: <20160827131224.27279.91467.70210239@psf.io> https://hg.python.org/jython/rev/1d7c5ac12419 changeset: 7941:1d7c5ac12419 user: Jeff Allen date: Thu Jun 09 23:05:09 2016 +0100 summary: Refactor array-based PyBuffer implementations. Quite substantial change to the class hierarchy for the array-based buffers to permit sharing with a parallel one using ByteBuffer. The ByteBuffer implementation is not in this commit (see later). There is a bug in copyFrom, when copying between buffers exported by the same object, that causes test_memoryview to fail. files: src/org/python/core/PyBUF.java | 20 +- src/org/python/core/PyBuffer.java | 81 +- src/org/python/core/buffer/BaseArrayBuffer.java | 279 ++++ src/org/python/core/buffer/BaseBuffer.java | 608 ++++++--- src/org/python/core/buffer/SimpleBuffer.java | 104 +- src/org/python/core/buffer/SimpleStringBuffer.java | 57 +- src/org/python/core/buffer/SimpleWritableBuffer.java | 72 +- src/org/python/core/buffer/Strided1DBuffer.java | 134 +- src/org/python/core/buffer/Strided1DWritableBuffer.java | 59 +- src/org/python/core/buffer/ZeroByteBuffer.java | 54 +- tests/java/org/python/core/PyBufferTest.java | 185 ++- tests/java/org/python/core/PyBufferTestSupport.java | 2 +- 12 files changed, 1021 insertions(+), 634 deletions(-) diff --git a/src/org/python/core/PyBUF.java b/src/org/python/core/PyBUF.java --- a/src/org/python/core/PyBUF.java +++ b/src/org/python/core/PyBUF.java @@ -51,17 +51,17 @@ int[] getShape(); /** - * The number of units (bytes) stored in each indexable item. + * The number of bytes stored in each indexable item. * - * @return the number of units (bytes) comprising each item. + * @return the number of bytes comprising each item. */ int getItemsize(); /** - * The total number of units (bytes) stored, which will be the product of the elements of the - * shape array, and the item size in units. + * The total number of bytes represented by the view, which will be the product of the elements of the + * shape array, and the item size in bytes. * - * @return the total number of units stored. + * @return the total number of bytes represented. */ int getLen(); @@ -143,7 +143,7 @@ static final int STRIDES = 0x0010 | ND; /** * A constant used by the consumer in its call to {@link BufferProtocol#getBuffer(int)} to - * specify that it will assume C-order organisation of the units. getBuffer will + * specify that it will assume C-order organisation of the items. getBuffer will * raise an exception if the exporter's buffer is not C-ordered. C_CONTIGUOUS * implies STRIDES. */ @@ -152,14 +152,14 @@ static final int C_CONTIGUOUS = 0x0020 | STRIDES; /** * A constant used by the consumer in its call to {@link BufferProtocol#getBuffer(int)} to - * specify that it will assume Fortran-order organisation of the units. getBuffer + * specify that it will assume Fortran-order organisation of the items. getBuffer * will raise an exception if the exporter's buffer is not Fortran-ordered. * F_CONTIGUOUS implies STRIDES. */ static final int F_CONTIGUOUS = 0x0040 | STRIDES; /** * A constant used by the consumer in its call to {@link BufferProtocol#getBuffer(int)} to - * specify that it will assume a contiguous organisation of the units, but will enquire which + * specify that it will assume a contiguous organisation of the items, but will enquire which * organisation it actually is. * * getBuffer will raise an exception if the exporter's buffer is not contiguous. @@ -244,13 +244,13 @@ static final int NAVIGATION = SIMPLE | ND | STRIDES | INDIRECT; /** * A constant used by the exporter in processing {@link BufferProtocol#getBuffer(int)} to check - * for assumed C-order organisation of the units. + * for assumed C-order organisation of the items. * C_CONTIGUOUS = IS_C_CONTIGUOUS | STRIDES. */ static final int IS_C_CONTIGUOUS = C_CONTIGUOUS & ~STRIDES; /** * A constant used by the exporter in processing {@link BufferProtocol#getBuffer(int)} to check - * for assumed C-order Fortran-order organisation of the units. + * for assumed C-order Fortran-order organisation of the items. * F_CONTIGUOUS = IS_F_CONTIGUOUS | STRIDES. */ static final int IS_F_CONTIGUOUS = F_CONTIGUOUS & ~STRIDES; diff --git a/src/org/python/core/PyBuffer.java b/src/org/python/core/PyBuffer.java --- a/src/org/python/core/PyBuffer.java +++ b/src/org/python/core/PyBuffer.java @@ -106,41 +106,40 @@ * further study.) * * @param dest destination byte array - * @param destPos index in the destination array of the byte [0] + * @param destPos byte-index in the destination array of the byte [0] * @throws IndexOutOfBoundsException if the destination cannot hold it */ void copyTo(byte[] dest, int destPos) throws IndexOutOfBoundsException, PyException; /** - * Copy a simple slice of the buffer to the destination byte array, defined by a starting index - * and length in the source buffer. This may validly be done only for a one-dimensional buffer, - * as the meaning of the starting index is otherwise not defined. The length (like the source - * index) is in source buffer items: length*itemsize bytes will be occupied - * in the destination. + * Copy a simple slice of the buffer-view to the destination byte array, defined by a starting + * item-index in the source buffer and the count of items to copy. This may validly + * be done only for a one-dimensional buffer, as the meaning of the starting item-index is + * otherwise not defined. count*itemsize bytes will be occupied in the destination. * - * @param srcIndex starting index in the source buffer + * @param srcIndex starting item-index in the source buffer * @param dest destination byte array - * @param destPos index in the destination array of the item [0,...] - * @param length number of items to copy + * @param destPos byte-index in the destination array of the source item [0,...] + * @param count number of items to copy * @throws IndexOutOfBoundsException if access out of bounds in source or destination */ - void copyTo(int srcIndex, byte[] dest, int destPos, int length) // mimic arraycopy args + void copyTo(int srcIndex, byte[] dest, int destPos, int count) // mimic arraycopy args throws IndexOutOfBoundsException, PyException; /** - * Copy bytes from a slice of a (Java) byte array into the buffer. This may validly be done only - * for a one-dimensional buffer, as the meaning of the starting index is not otherwise defined. - * The length (like the destination index) is in buffer items: - * length*itemsize bytes will be read from the source. + * Copy from a slice of a (Java) byte array into the buffer starting at a given destination + * item-index. This may validly be done only for a one-dimensional buffer, as the meaning of the + * destination index is not otherwise defined. count*itemsize bytes will be read + * from the source. * * @param src source byte array * @param srcPos location in source of first byte to copy - * @param destIndex starting index in the destination (i.e. this) - * @param length number of bytes to copy in + * @param destIndex starting item-index in the destination (i.e. this) + * @param count number of items to copy in * @throws IndexOutOfBoundsException if access out of bounds in source or destination * @throws PyException (TypeError) if read-only buffer */ - void copyFrom(byte[] src, int srcPos, int destIndex, int length) // mimic arraycopy args + void copyFrom(byte[] src, int srcPos, int destIndex, int count) // mimic arraycopy args throws IndexOutOfBoundsException, PyException; /** @@ -202,10 +201,10 @@ * * @param flags specifying features demanded and the navigational capabilities of the consumer * @param start index in the current buffer - * @param length number of items in the required slice + * @param count number of items in the required slice * @return a buffer representing the slice */ - public PyBuffer getBufferSlice(int flags, int start, int length); + public PyBuffer getBufferSlice(int flags, int start, int count); /** * Get a PyBuffer that represents a slice of the current one described in terms of @@ -217,7 +216,7 @@ * Suppose that x(i) denotes the ith element of the current buffer, that is, the * byte retrieved by this.byteAt(i) or the unit indicated by * this.getPointer(i). A request for a slice where start = s, - * length = N and stride = m, results in a buffer + * count = N and stride = m, results in a buffer * y such that y(k) = x(s+km) where k=0..(N-1). In Python terms, this is * the slice x[s : s+(N-1)m+1 : m] (if m>0) or the slice x[s : s+(N-1)m-1 : * m] (if m<0). Implementations should check that this range is entirely within @@ -226,29 +225,31 @@ * In a simple buffer backed by a contiguous byte array, the result is a strided PyBuffer on the * same storage but where the offset is adjusted by s and the stride is as supplied. If * the current buffer is already strided and/or has an item size larger than single bytes, the - * new start index, length and stride will be translated + * new start index, count and stride will be translated * from the arguments given, through this buffer's stride and item size. The caller always * expresses start and strides in terms of the abstract view of this * buffer. * * @param flags specifying features demanded and the navigational capabilities of the consumer * @param start index in the current buffer - * @param length number of items in the required slice + * @param count number of items in the required slice * @param stride index-distance in the current buffer between consecutive items in the slice * @return a buffer representing the slice */ - public PyBuffer getBufferSlice(int flags, int start, int length, int stride); + public PyBuffer getBufferSlice(int flags, int start, int count, int stride); // java.nio access to actual storage // /** * Obtain a {@link java.nio.ByteBuffer} giving access to the bytes that hold the data being - * exported to the consumer. The position of the buffer is at the item with zero index, the - * limit of the buffer is one beyond the largest valid index, and the mark is undefined. + * exported by the original object. The position of the buffer is at the first byte of the item + * with zero index (quite possibly not the lowest valid byte-index), the limit of the buffer is + * beyond the largest valid byte index, and the mark is undefined. *

- * For a one-dimensional contiguous buffer, assuming the following client code where - * obj has type BufferProtocol: + * For a one-dimensional contiguous buffer, the limit is one byte beyond the last item, so that + * consecutive reads from the ByteBuffer return the data in order. Assuming the + * following client code where obj has type BufferProtocol: * *

      * PyBuffer a = obj.getBuffer(PyBUF.SIMPLE);
@@ -259,18 +260,13 @@
      * the item with index k is in bb at positions
      * bb.pos()+k*itemsize to bb.pos()+(k+1)*itemsize - 1 inclusive. And
      * if itemsize==1, the item is simply the byte at position bb.pos()+k.
-     * The buffer limit is set to the first byte beyond the valid data. A block read or write will
-     * therefore access the contents sequentially. In a one-dimensional contiguous buffer (only) it
-     * is safe to rely on bb.remaining() to obtain the number of bytes representing the
-     * object state.
      * 

* If the buffer is multidimensional or non-contiguous (strided), the buffer position is still - * the (first byte of) the item at index [0] or [0,...,0], and the - * limit is one item beyond the valid data. However, it is necessary to navigate bb - * using the shape, strides and maybe suboffsets provided - * by the API. + * the (first byte of) the item at index [0] or [0,...,0]. However, it + * is necessary to navigate bb using the shape, strides + * and maybe suboffsets provided by the API. * - * @return a ByteBuffer onto the exported data contents. + * @return a ByteBuffer onto the exported data contents. */ ByteBuffer getNIOByteBuffer(); @@ -298,19 +294,18 @@ */ ByteBuffer getNIOByteBuffer(int... indices); - // Direct access to actual storage (deprecated) - // - /** - * Determine whether the exporter is able to offer direct access to the exported storage as a - * Java byte array (through the API that involves class {@link Pointer}), or only supports the + * Report whether the exporter is able to offer direct access to the exported storage as a Java + * byte array (through the API that involves class {@link Pointer}), or only supports the * abstract API. See also {@link PyBUF#AS_ARRAY}. * * @return true if array access is supported, false if it is not. */ - // XXX Pending: @Deprecated boolean hasArray(); + // Direct access to actual storage (deprecated) + // + /** * A class that references a byte[] array and a particular offset within it, as the * return type for methods that give direct access to byte-oriented data exported by a Python @@ -410,7 +405,7 @@ * free to navigate the underlying buffer b.storage without respecting these * boundaries. If the buffer is non-contiguous, the above description is still valid (since a * multi-byte item must itself be contiguously stored), but in any additional navigation of - * b.storage[] to other units, the client must use the shape, strides and + * b.storage[] to other items, the client must use the shape, strides and * sub-offsets provided by the API. Normally one starts b = a.getBuf() in order to * establish the offset of index [0,...,0]. * diff --git a/src/org/python/core/buffer/BaseArrayBuffer.java b/src/org/python/core/buffer/BaseArrayBuffer.java new file mode 100644 --- /dev/null +++ b/src/org/python/core/buffer/BaseArrayBuffer.java @@ -0,0 +1,279 @@ +package org.python.core.buffer; + +import java.nio.ByteBuffer; + +import org.python.core.PyBUF; +import org.python.core.PyBuffer; +import org.python.core.PyException; + +/** + * Base implementation of the Buffer API for when the storage implementation is byte[]. + * The description of {@link BaseBuffer} mostly applies. Methods provided or overridden here are + * appropriate to 1-dimensional arrays backed by byte[]. + * + */ +public abstract class BaseArrayBuffer extends BaseBuffer implements PyBuffer { + + /** + * Reference to the underlying byte[] storage that the exporter is sharing with the + * consumer. The data need not occupy the whole array: in the constructor of a particular type + * of buffer, the exporter usually indicates an offset to the first significant byte and length + * (contiguous cases) or the index in storage that should be treated as the item + * with index zero (retrieved say by buf.byteAt(0)). + */ + protected byte[] storage; + + /** + * Construct an instance of BaseArrayBuffer in support of a sub-class, specifying + * the 'feature flags', or at least a starting set to be adjusted later. These are the features + * of the buffer exported, not the flags that form the consumer's request. The buffer will be + * read-only unless {@link PyBUF#WRITABLE} is set in the feature flags. {@link PyBUF#FORMAT} and + * {@link PyBUF#AS_ARRAY} are implicitly added to the feature flags. The navigation arrays are + * all null, awaiting action by the sub-class constructor. To complete initialisation, the + * sub-class normally must assign: the buffer ( {@link #storage}, {@link #index0}), and the + * navigation arrays ({@link #shape}, {@link #strides}), and call + * {@link #checkRequestFlags(int)} passing the consumer's request flags. + * + * @param featureFlags bit pattern that specifies the actual features allowed/required + */ + protected BaseArrayBuffer(int featureFlags) { + super(featureFlags | AS_ARRAY); + } + + @Override + protected int getSize() { + return shape[0]; + } + + @Override + public int getLen() { + return shape[0] * getItemsize(); + } + + @Override + protected byte byteAtImpl(int byteIndex) throws IndexOutOfBoundsException { + return storage[byteIndex]; + } + + @Override + protected void storeAtImpl(byte value, int byteIndex) throws IndexOutOfBoundsException, + PyException { + checkWritable(); + storage[byteIndex] = value; + } + + @Override + protected int byteIndex(int... indices) throws IndexOutOfBoundsException { + // BaseBuffer implementation can be simplified since if indices.length!=1 we error. + checkDimension(indices.length); // throws if != 1 + return byteIndex(indices[0]); + } + + /** + * {@inheritDoc} + *

+ * Specialised to one-dimensional, possibly strided buffer. + */ + @Override + protected int calcGreatestIndex() { + int stride = strides[0]; + if (stride == 1) { + return index0 + shape[0] - 1; + } else if (stride > 0) { + return index0 + (shape[0] - 1) * stride; + } else { + return index0 - 1; + } + } + + /** + * {@inheritDoc} + *

+ * Specialised to one-dimensional, possibly strided buffer. + */ + @Override + protected int calcLeastIndex() { + int stride = strides[0]; + if (stride < 0) { + return index0 + (shape[0] - 1) * stride; + } else { + return index0; + } + } + + /** + * {@inheritDoc} + *

+ * The implementation in BaseArrayBuffer deals with the general one-dimensional + * case of arbitrary item size and stride. + */ + @Override + public void copyTo(int srcIndex, byte[] dest, int destPos, int count) + throws IndexOutOfBoundsException { + + if (count > 0) { + // Pick up attributes necessary to choose an efficient copy strategy + int itemsize = getItemsize(); + int stride = getStrides()[0]; + int skip = stride - itemsize; + int s = byteIndex(srcIndex); + + // Strategy depends on whether items are laid end-to-end contiguously or there are gaps + if (skip == 0) { + // stride == itemsize: straight copy of contiguous bytes + System.arraycopy(storage, s, dest, destPos, count * itemsize); + } else { + int limit = s + count * stride, d = destPos; + if (itemsize == 1) { + // Non-contiguous copy: single byte items + for (; s != limit; s += stride) { + dest[d++] = storage[s]; + } + } else { + // Non-contiguous copy: each time, copy itemsize bytes then skip + for (; s != limit; s += skip) { + int t = s + itemsize; + while (s < t) { + dest[d++] = storage[s++]; + } + } + } + } + } + } + + /** + * {@inheritDoc} + *

+ * The default implementation in BaseArrayBuffer deals with the general + * one-dimensional case of arbitrary item size and stride. + */ + @Override + public void copyFrom(byte[] src, int srcPos, int destIndex, int count) + throws IndexOutOfBoundsException, PyException { + + checkWritable(); + + if (count > 0) { + // Pick up attributes necessary to choose an efficient copy strategy + int itemsize = getItemsize(); + int stride = getStrides()[0]; + int skip = stride - itemsize; + int d = byteIndex(destIndex); + + // Strategy depends on whether items are laid end-to-end or there are gaps + if (skip == 0) { + // Straight copy of contiguous bytes + System.arraycopy(src, srcPos, storage, d, count * itemsize); + } else { + // Non-contiguous copy: single byte items + int limit = d + count * stride, s = srcPos; + if (itemsize == 1) { + for (; d != limit; d += stride) { + storage[d] = src[s++]; + } + } else { + // Non-contiguous copy: itemsize bytes then skip to next item + for (; d != limit; d += skip) { + int t = d + itemsize; + while (d < t) { + storage[d++] = src[s++]; + } + } + } + } + } + } + + @Override + public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { + if (src instanceof BaseArrayBuffer) { + copyFromArrayBuffer((BaseArrayBuffer)src); + } else { + super.copyFrom(src); + } + } + + private void copyFromArrayBuffer(BaseArrayBuffer src) throws IndexOutOfBoundsException, + PyException { + + checkWritable(); + src.checkDimension(1); + + int itemsize = getItemsize(); + int count = getSize(); + + // Block operation if different item or overall size (permit reshape) + if (src.getItemsize() != itemsize || src.getSize() != count) { + throw differentStructure(); + } + + for (int i = 0; i < count; i++) { + int s = src.byteIndex(i), d = byteIndex(i); + for (int j = 0; j < itemsize; j++) { + storage[d++] = src.byteAtImpl(s++); + } + } + } + + /** + * Copy blocks of bytes, equally spaced in the source array, to locations equally spaced in the + * destination array, which may be the same array. The byte at + * src[srcPos+k*srcStride+j] will be copied to + * dst[dstPos+k*dstStride+j] for 0≤k<count and + * 0≤j<size. When the source and destination are the same array, the method + * deals correctly with the risk that a byte gets written under the alias dst[x] + * before it should have been copied referenced as src[y]. + * + * @param size of the blocks of bytes + * @param src the source array + * @param srcPos the position of the first block in the source + * @param srcStride the interval between the start of each block in the source + * @param dst the destination array + * @param dstPos the position of the first block in the destination + * @param dstStride the interval between the start of each block in the destination + * @param count the number of blocks to copy + */ + private static void slicedArrayCopy(int size, byte[] src, int srcPos, int srcStride, + byte[] dst, int dstPos, int dstStride, int count) {} + + @Override + protected ByteBuffer getNIOByteBufferImpl() { + // The buffer spans the whole storage, which may include data not in the view + ByteBuffer b = ByteBuffer.wrap(storage); + // Return as read-only if it is. + return isReadonly() ? b.asReadOnlyBuffer() : b; + } + + /** + * {@inheritDoc} + *

+ * BaseArrayBuffer provides a reference to the storage array even when the buffer + * is intended not to be writable. There can be no enforcement of read-only character once a + * reference to the byte array has been handed out. + */ + @SuppressWarnings("deprecation") + @Override + public Pointer getBuf() { + return new Pointer(storage, index0); + } + + /** + * {@inheritDoc} + *

+ * Specialised in BaseArrayBuffer to one dimension. + */ + @Override + public boolean isContiguous(char order) { + if ("CFA".indexOf(order) < 0) { + return false; + } else { + if (getShape()[0] < 2) { + return true; + } else { + return getStrides()[0] == getItemsize(); + } + } + } + +} diff --git a/src/org/python/core/buffer/BaseBuffer.java b/src/org/python/core/buffer/BaseBuffer.java --- a/src/org/python/core/buffer/BaseBuffer.java +++ b/src/org/python/core/buffer/BaseBuffer.java @@ -9,34 +9,35 @@ import org.python.core.PyException; /** - * Base implementation of the Buffer API providing variables and accessors for the navigational + * Base implementation of the Buffer API providing variables and accessors for the navigation * arrays, methods for expressing and checking the buffer request flags, methods and mechanism for * get-release counting, boilerplate error checks and their associated exceptions, and default * implementations of some methods for access to the buffer content. The design aim is to ensure * unglamorous common code need only be implemented once. *

- * Where provided, the buffer access methods are appropriate to 1-dimensional arrays where the units - * are single bytes, stored contiguously. Sub-classes that deal with N-dimensional arrays, - * non-contiguous storage and items that are not single bytes must override the default - * implementations. + * This class leaves undefined the storage mechanism for the bytes (typically byte[] or + * java.nio.ByteBuffer). A concrete class that extends this one must provide elementary + * accessors {@link #byteAtImpl(int)}, {@link #storeAtImpl(byte, int)} that abstract this storage, a + * factory {@link #getNIOByteBufferImpl()} for ByteBuffers that wrap the storage, and a + * factory for slices {@link #getBufferSlice(int, int, int, int)}. The constructor must specify the + * feature flags (see {@link #BaseBuffer(int)}), set {@link #index0}, {@link #shape} and + * {@link #strides}, and finally check the client capabilities with {@link #checkRequestFlags(int)}. + * Sub-classes intended to represent slices of exporters that must count their exports as part of a + * locking protocol, as does bytearray, must override {@link #getRoot()} so that a + * buffer view {@link #release()} on a slice, propagates to the buffer view that provided it. + *

+ * Access methods provided here necessarily work with the abstracted {@link #byteAtImpl(int)}, + * {@link #storeAtImpl(byte, int)} interface, but subclasses are able to override them with more + * efficient versions that employ knowledge of the particular storage type used. The provided buffer + * access methods may be restricted to 1-dimensional arrays where the units are single bytes, stored + * contiguously. Sub-classes that deal with N-dimensional arrays, non-contiguous storage and items + * that are not single bytes must sometimes override the default implementations. *

* This base implementation is writable only if {@link PyBUF#WRITABLE} is in the feature flags - * passed to the constructor. Otherwise, all methods for write access raise a - * BufferError read-only exception and {@link #isReadonly()} returns true. - * Sub-classes can follow the same pattern, setting {@link PyBUF#WRITABLE} in the constructor and, - * if they have to, overriding the operations that write (storeAt and - * copyFrom). The recommended pattern is: - * - *

- * if (isReadonly()) {
- *     throw notWritable();
- * }
- * // ... implementation of the write operation
- * 
- * Another approach, used in the standard library, is to have distinct classes for the writable and - * read-only variants. The implementors of simple buffers will find it efficient to override the - * generic access methods to which performance might be sensitive, with a calculation specific to - * their actual type. + * passed to the constructor. Otherwise, all methods for write access raise a TypeError + * and {@link #isReadonly()} returns true. However, a client intending to write should + * have presented {@link PyBUF#WRITABLE} in its client request flags when getting the buffer, and + * been prevented by a BufferError exception at that point. *

* At the time of writing, only one-dimensional buffers of item size one are used in the Jython * core. @@ -59,20 +60,16 @@ protected int[] strides; /** - * Reference to the underlying byte[] storage that the exporter is sharing with the - * consumer. The data need not occupy the whole array: in the constructor of a particular type - * of buffer, the exporter usually indicates an offset to the first significant byte and length - * (contiguous cases) or the index in storage that should be treated as the item - * with index zero (retrieved say by {@link #byteAt(int)}). + * The strides array for a contiguous byte buffer.. */ - // XXX Pending change of implementation to ByteBuffer - protected byte[] storage; + protected static final int[] CONTIG_STRIDES = {1}; /** - * Absolute index in storage of item[0]. In one dimension, for a - * positive stride this is equal to the offset of the first byte used in - * {@link #storage}, and for a negative stride it is the last. In an N-dimensional - * buffer with strides of mixed sign, it could be anywhere in the data. + * Absolute byte-index in the storage of item[0]. In one dimension, for a positive + * stride this is equal to the offset of the first byte used in whatever + * byte-storage is provided, and for a negative stride it is the first byte of the + * last item. In an N-dimensional buffer with strides of mixed sign, it could be anywhere in the + * data. */ protected int index0; @@ -92,8 +89,8 @@ * the consumer does not specify that it will use a navigation array the buffer requires. *

* In order to support efficient checking with {@link #checkRequestFlags(int)} we store a - * mutilated version of the apparent featureFlags in which the non-navigational - * flags are inverted. The syndrome S of the error is computed as follows. Let + * mutilated version of the apparent featureFlags in which the non-navigation flags + * are inverted. The syndrome S of the error is computed as follows. Let * N=1 where we are dealing with a navigation flag, let F be a buffer * feature flag, and let X be the consumer request flags. * @@ -126,20 +123,20 @@ private int gFeatureFlags = ~NAVIGATION; // featureFlags = 0 /** - * Construct an instance of BaseBuffer in support of a sub-class, specifying the 'feature - * flags', or at least a starting set to be adjusted later. These are the features of the buffer - * exported, not the flags that form the consumer's request. The buffer will be read-only unless - * {@link PyBUF#WRITABLE} is set in the feature flags. {@link PyBUF#FORMAT} and - * {@link PyBUF#AS_ARRAY} are implicitly added to the feature flags. The navigation arrays are - * all null, awaiting action by the sub-class constructor. To complete initialisation, the - * sub-class normally must assign: the buffer ( {@link #storage}, {@link #index0}), and the - * navigation arrays ({@link #shape}, {@link #strides}), and call - * {@link #checkRequestFlags(int)} passing the consumer's request flags. + * Construct an instance of BaseBuffer in support of a sub-class, specifying the + * 'feature flags', or at least a starting set to be adjusted later. These are the features of + * the buffer exported, not the flags that form the consumer's request. The buffer will be + * read-only unless {@link PyBUF#WRITABLE} is set in the feature flags. {@link PyBUF#FORMAT} is + * implicitly added to the feature flags. The navigation arrays are all null, awaiting action by + * the sub-class constructor. To complete initialisation, the sub-class normally must create its + * own wrapped byte-storage, assign {@link #index0}) and the navigation arrays ( {@link #shape}, + * {@link #strides}), and call {@link #checkRequestFlags(int)} passing the consumer's request + * flags. * * @param featureFlags bit pattern that specifies the actual features allowed/required */ protected BaseBuffer(int featureFlags) { - setFeatureFlags(featureFlags | FORMAT | AS_ARRAY); + setFeatureFlags(featureFlags | FORMAT); } /** @@ -176,6 +173,17 @@ } /** + * Remove features from this buffer expressed using the constants defined in {@link PyBUF}, + * clearing individual flags specified while leaving others already set. Equivalent to + * setFeatureFlags(~flags & getFeatureFlags()). + * + * @param flags to clear within the feature flags + */ + protected final void removeFeatureFlags(int flags) { + setFeatureFlags(~flags & getFeatureFlags()); + } + + /** * General purpose method to check the consumer request flags (typically the argument to * {@link BufferProtocol#getBuffer(int)}) against the feature flags (see * {@link #getFeatureFlags()}) that characterise the features of the buffer, and to raise an @@ -188,7 +196,7 @@ * capabilities this type (or instance) buffer actually has. It is an error, for the consumer to * specify in its request a feature that the buffer does not offer. *

- * In a subset of the flags, the consumer specifies the set of navigational arrays ( + * In a subset of the flags, the consumer specifies the set of navigation arrays ( * shape, strides, and suboffsets) it intends to use in * navigating the buffer. When the buffer implementation calls this check method, it has already * specified in {@link #setFeatureFlags(int)} what navigation is necessary for the consumer to @@ -211,17 +219,11 @@ @Override public boolean isReadonly() { - // WRITABLE is a non-navigational flag, so is inverted in gFeatureFlags + // WRITABLE is a non-navigation flag, so is inverted in gFeatureFlags return (gFeatureFlags & WRITABLE) != 0; // i.e. featureFlags & WRITABLE is false } @Override - public boolean hasArray() { - // AS_ARRAY is a non-navigational flag, so is inverted in gFeatureFlags - return (gFeatureFlags & AS_ARRAY) == 0; // i.e. featureFlags & AS_ARRAY is true - } - - @Override public int getNdim() { return shape.length; } @@ -232,64 +234,137 @@ return shape; } + // XXX Consider making this part of the PyBUF interface + protected int getSize() { + final int N = shape.length; + int size = shape[0]; + for (int k = 1; k < N; k++) { + size *= shape[k]; + } + return size; + } + + @Override + public int getLen() { + final int N = shape.length; + int len = getItemsize(); + for (int k = 0; k < N; k++) { + len *= shape[k]; + } + return len; + } + + /** + * Retrieve the byte at the given index in the underlying storage treated as a flat sequence of + * bytes. This byte-index will have been computed from the item index (which may have been + * multi-dimensional), taking into account {@link #index0}, {@link #shape}, {@link #strides}, + * and the item size. The caller is responsible for validating the original item-index and + * raising (typically) an IndexOutOfBoundsException. Misuse of this method may + * still result in unchecked exceptions characteristic of the storage implementation. + * + * @param byteIndex byte-index of location to retrieve + * @return the byte at byteIndex + */ + abstract protected byte byteAtImpl(int byteIndex) throws IndexOutOfBoundsException; + + /** + * Store the byte at the given index in the underlying storage treated as a flat sequence of + * bytes. This byte-index will have been computed from the item index (which may have been + * multi-dimensional), taking into account {@link #index0}, {@link #shape}, {@link #strides}, + * and the item size. The caller is responsible for validating the original item-index and + * raising (typically) an IndexOutOfBoundsException. Misuse of this method may + * still result in unchecked exceptions characteristic of the storage implementation. This + * method must implement the check for read-only character, raising a BufferError + * in the case of a violation. + * + * @param value to store + * @param byteIndex byte-index of location to retrieve + * @throws PyException(BufferError) if this object is read-only. + */ + abstract protected void storeAtImpl(byte value, int byteIndex) + throws IndexOutOfBoundsException, PyException; + /** * {@inheritDoc} *

- * The default implementation in BaseBuffer deals with the general one-dimensional - * case, with any item size and stride. + * The BaseBuffer implementation delegates to {@link #byteAtImpl(int)} via + * byteAtImpl(byteIndex(index)). */ @Override - public int getLen() { - // Correct if one-dimensional. Override with itemsize*product(shape). - return shape[0] * getItemsize(); - } - - @Override public byte byteAt(int index) throws IndexOutOfBoundsException { - return storage[calcIndex(index)]; - } - - @Override - public int intAt(int index) throws IndexOutOfBoundsException { - return 0xff & byteAt(index); - } - - @Override - public void storeAt(byte value, int index) throws IndexOutOfBoundsException, PyException { - if (isReadonly()) { - throw notWritable(); - } - storage[calcIndex(index)] = value; + return byteAtImpl(byteIndex(index)); } /** - * Convert an item index (for a one-dimensional buffer) to an absolute byte index in the actual - * storage being shared by the exporter. See {@link #calcIndex(int...)} for discussion. - * - * @param index from consumer - * @return index in actual storage + * {@inheritDoc} + *

+ * The BaseBuffer implementation delegates to {@link #byteAtImpl(int)} via + * byteAtImpl(byteIndex(index)), cast unsigned to an int. */ - protected int calcIndex(int index) throws IndexOutOfBoundsException { - // Treat as one-dimensional - return index0 + index * getStrides()[0]; + @Override + public int intAt(int index) throws IndexOutOfBoundsException { + return 0xff & byteAtImpl(byteIndex(index)); } + /** + * {@inheritDoc} + *

+ * The BaseBuffer implementation delegates to {@link #storeAtImpl(byte, int)} via + * storeAtImpl(value, byteIndex(index)). + */ + @Override + public void storeAt(byte value, int index) throws IndexOutOfBoundsException, PyException { + storeAtImpl(value, byteIndex(index)); + } + + /** + * {@inheritDoc} + *

+ * The BaseBuffer implementation delegates to {@link #byteAtImpl(int)} via + * byteAtImpl(byteIndex(indices)). + */ @Override public byte byteAt(int... indices) throws IndexOutOfBoundsException { - return storage[calcIndex(indices)]; + return byteAtImpl(byteIndex(indices)); } + /** + * {@inheritDoc} + *

+ * The BaseBuffer implementation delegates to {@link #byteAtImpl(int)} via + * byteAtImpl(byteIndex(indices)), cast unsigned to an int. + */ @Override public int intAt(int... indices) throws IndexOutOfBoundsException { return 0xff & byteAt(indices); } + /** + * {@inheritDoc} + *

+ * The BaseBuffer implementation delegates to {@link #storeAtImpl(byte, int)} via + * storeAtImpl(value, byteIndex(indices)). + */ @Override public void storeAt(byte value, int... indices) throws IndexOutOfBoundsException, PyException { - if (isReadonly()) { - throw notWritable(); + storeAtImpl(value, byteIndex(indices)); + } + + /** + * Convert an item index (for a one-dimensional buffer) to a checked absolute byte index in the + * actual storage being shared by the exporter. See {@link #byteIndex(int[])} for discussion. + * + * @param index item-index from consumer + * @return corresponding byte-index in actual storage + * @throws IndexOutOfBoundsException if the index <0 or ≥shape[0] + */ + // XXX Consider making this part of the PyBuffer interface + protected int byteIndex(int index) throws IndexOutOfBoundsException { + // Treat as one-dimensional + if (index < 0 || index >= shape[0]) { + throw new IndexOutOfBoundsException(); } - storage[calcIndex(indices)] = value; + return index0 + index * strides[0]; } /** @@ -298,25 +373,28 @@ * to allow a sub-class to define, in one place, an indexing calculation that maps the index as * provided by the consumer into an index in the storage known to the buffer. *

- * In the usual case where the storage is referenced via the {@link #storage} and - * {@link #index0} members, the buffer implementation may use storage[calcIndex(i)] + * In the usual case where the storage is referenced via a storage member and + * {@link #index0} member, the buffer implementation may use storage[byteIndex(i)] * to reference the (first byte of) the item x[i]. This is what the default implementation of - * accessors in BaseBuffer will do. In the simplest cases, calling - * calcIndex may be an overhead to avoid, and an implementation will specialise the + * accessors in BaseArrayBuffer will do. In the simplest cases, calling + * byteIndex may be an overhead to avoid, and an implementation will specialise the * accessors. The default implementation here is suited to N-dimensional arrays. * - * @param indices of the item from the consumer - * @return corresponding absolute index in storage + * @param indices n-dimensional item-index from consumer + * @return corresponding byte-index in actual storage + * @throws IndexOutOfBoundsException if any index <0 or ≥shape[i] */ - protected int calcIndex(int... indices) throws IndexOutOfBoundsException { + // XXX Consider making this part of the PyBuffer interface + protected int byteIndex(int... indices) throws IndexOutOfBoundsException { final int N = checkDimension(indices); // In general: index0 + sum(k=0,N-1) indices[k]*strides[k] int index = index0; - if (N > 0) { - int[] strides = getStrides(); - for (int k = 0; k < N; k++) { - index += indices[k] * strides[k]; + for (int k = 0; k < N; k++) { + int ik = indices[k]; + if (ik < 0 || ik >= shape[k]) { + throw new IndexOutOfBoundsException(); } + index += ik * strides[k]; } return index; } @@ -324,24 +402,22 @@ /** * Calculate the absolute byte index in the storage array of the last item of the exported data * (if we are not using indirection). This is the greatest value attained by - * {@link #calcIndex(int...)}. The first byte not used will be one itemsize more + * {@link #byteIndex(int...)}. The first byte not used will be one itemsize more * than the returned value. * * @return greatest absolute index in storage */ - protected int calcGreatestIndex() throws IndexOutOfBoundsException { + protected int calcGreatestIndex() { final int N = shape.length; // If all the strides are positive, the maximal value is found from: // index = index0 + sum(k=0,N-1) (shape[k]-1)*strides[k] // but in general, for any k where strides[k]<=0, the term should be zero. int index = index0; - if (N > 0) { - int[] strides = getStrides(); - for (int k = 0; k < N; k++) { - int stride = strides[k]; - if (stride > 0) { - index += (shape[k] - 1) * stride; - } + int[] strides = getStrides(); + for (int k = 0; k < N; k++) { + int stride = strides[k]; + if (stride > 0) { + index += (shape[k] - 1) * stride; } } return index; @@ -350,23 +426,21 @@ /** * Calculate the absolute byte index in the storage array of the first item of the exported data * (if we are not using indirection). This is the least value attained by - * {@link #calcIndex(int...)}. + * {@link #byteIndex(int...)}. * * @return least absolute index in storage */ - protected int calcLeastIndex() throws IndexOutOfBoundsException { + protected int calcLeastIndex() { final int N = shape.length; // If all the strides are positive, the maximal value is just index0, // but in general, we must allow strides[k]<=0 for some k: // index = index0 + sum(k=0,N-1) (strides[k]<0) ? (shape[k]-1)*strides[k] : 0 int index = index0; - if (N > 0) { - int[] strides = getStrides(); - for (int k = 0; k < N; k++) { - int stride = strides[k]; - if (stride < 0) { - index += (shape[k] - 1) * stride; - } + int[] strides = getStrides(); + for (int k = 0; k < N; k++) { + int stride = strides[k]; + if (stride < 0) { + index += (shape[k] - 1) * stride; } } return index; @@ -381,147 +455,97 @@ @Override public void copyTo(byte[] dest, int destPos) throws IndexOutOfBoundsException { // Note shape[0] is the number of items in the array - copyTo(0, dest, destPos, shape[0]); + copyTo(0, dest, destPos, getSize()); } /** * {@inheritDoc} *

* The default implementation in BaseBuffer deals with the general one-dimensional - * case of arbitrary item size and stride. + * case of arbitrary item size and stride, but is unable to optimise access to sequential bytes. */ @Override - public void copyTo(int srcIndex, byte[] dest, int destPos, int length) - throws IndexOutOfBoundsException { + public void copyTo(int srcIndex, byte[] dest, int destPos, int count) + throws IndexOutOfBoundsException, PyException { - // Data is here in the buffers - int s = calcIndex(srcIndex); - int d = destPos; + checkDimension(1); - // Pick up attributes necessary to choose an efficient copy strategy int itemsize = getItemsize(); - int stride = getStrides()[0]; - int skip = stride - itemsize; + int s = srcIndex, d = destPos; - // Strategy depends on whether items are laid end-to-end contiguously or there are gaps - if (skip == 0) { - // stride == itemsize: straight copy of contiguous bytes - System.arraycopy(storage, s, dest, d, length * itemsize); - - } else if (itemsize == 1) { - // Non-contiguous copy: single byte items - int limit = s + length * stride; - for (; s < limit; s += stride) { - dest[d++] = storage[s]; + if (itemsize == 1) { + // Single byte items + for (int i = 0; i < count; i++) { + dest[d++] = byteAt(s++); } - } else { - // Non-contiguous copy: each time, copy itemsize bytes then skip - int limit = s + length * stride; - for (; s < limit; s += skip) { - int t = s + itemsize; - while (s < t) { - dest[d++] = storage[s++]; + // Multi-byte items + for (int i = 0; i < count; i++) { + int p = byteIndex(s++); + for (int j = 0; j < itemsize; j++) { + dest[d++] = byteAtImpl(p + j); } } } - } /** * {@inheritDoc} *

* The default implementation in BaseBuffer deals with the general one-dimensional - * case of arbitrary item size and stride. + * case of arbitrary item size and stride, but is unable to optimise access to sequential bytes. */ @Override - public void copyFrom(byte[] src, int srcPos, int destIndex, int length) + public void copyFrom(byte[] src, int srcPos, int destIndex, int count) throws IndexOutOfBoundsException, PyException { - // Block operation if read-only - if (isReadonly()) { - throw notWritable(); - } + checkDimension(1); + checkWritable(); - // Data is here in the buffers - int s = srcPos; - int d = calcIndex(destIndex); + int itemsize = getItemsize(); + int d = destIndex, s = srcPos; - // Pick up attributes necessary to choose an efficient copy strategy - int itemsize = getItemsize(); - int stride = getStrides()[0]; - int skip = stride - itemsize; - - // Strategy depends on whether items are laid end-to-end or there are gaps - if (skip == 0) { - // Straight copy of contiguous bytes - System.arraycopy(src, srcPos, storage, d, length * itemsize); - - } else if (itemsize == 1) { - // Non-contiguous copy: single byte items - int limit = d + length * stride; - for (; d != limit; d += stride) { - storage[d] = src[s++]; + if (itemsize == 1) { + // Single byte items + for (int i = 0; i < count; i++) { + storeAt(src[s++], d++); } - } else { - // Non-contiguous copy: each time, copy itemsize bytes then skip - int limit = d + length * stride; - for (; d != limit; d += skip) { - int t = d + itemsize; - while (d < t) { - storage[d++] = src[s++]; + // Multi-byte items + for (int i = 0; i < count; i++) { + int p = byteIndex(d++); + for (int j = 0; j < itemsize; j++) { + storeAtImpl(src[s++], p++); } } } - } /** * {@inheritDoc} *

* The default implementation in BaseBuffer deals with the general one-dimensional - * case. + * case of arbitrary item size and stride, but is unable to optimise access to sequential bytes. */ @Override public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { - // Block operation if read-only and same length - if (isReadonly()) { - throw notWritable(); - } else if (src.getLen() != getLen() || src.getItemsize() != getItemsize()) { + checkWritable(); + + int itemsize = getItemsize(); + int count = getSize(); + + // Block operation if different item or overall size (permit reshape) + if (src.getItemsize() != itemsize || src.getLen() != count * itemsize) { throw differentStructure(); } - // Data is here in the buffers - int s = 0; - int d = calcIndex(0); - - // Pick up attributes necessary to choose an efficient copy strategy - int itemsize = getItemsize(); - int stride = getStrides()[0]; - - // Strategy depends on whether items are laid end-to-end or there are gaps - if (stride == itemsize) { - // Straight copy to contiguous bytes - src.copyTo(storage, d); - - } else if (itemsize == 1) { - // Non-contiguous copy: single byte items - int limit = d + src.getLen() * stride; - for (; d != limit; d += stride) { - storage[d] = src.byteAt(s++); - } - - } else { - // Non-contiguous copy: each time, copy itemsize bytes then skip - int limit = d + src.getShape()[0] * stride; - for (; d != limit; d += stride) { - Pointer srcItem = src.getPointer(s++); - System.arraycopy(srcItem.storage, srcItem.offset, storage, d, itemsize); - } + // XXX Re-think this using ByteBuffer when the API provides a byteIndex() method + assert itemsize == 1; + // XXX This only moves the first byte of each item + for (int i = 0; i < count; i++) { + storeAt(src.byteAt(i), i); } - } @Override @@ -536,7 +560,7 @@ } /** - * Allow an exporter to re-use a BaseBytes even if it has been "finally" released. Many + * Allow an exporter to re-use this object again even if it has been "finally" released. Many * sub-classes of BaseBytes can be re-used even after a final release by consumers, * simply by incrementing the exports count again: the navigation arrays and the * buffer view of the exporter's state all remain valid. We do not let consumers do this through @@ -574,6 +598,7 @@ if (--exports == 0) { // This is a final release. releaseAction(); + // XXX Consider adding release of root if root!=this so sliced need not } else if (exports < 0) { // Buffer already had 0 exports. (Put this right, in passing.) exports = 0; @@ -592,55 +617,79 @@ } @Override - public PyBuffer getBufferSlice(int flags, int start, int length) { - return getBufferSlice(flags, start, length, 1); + public PyBuffer getBufferSlice(int flags, int start, int count) { + return getBufferSlice(flags, start, count, 1); } // Let the sub-class implement - // @Override public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {} + // @Override public PyBuffer getBufferSlice(int flags, int start, int count, int stride) {} + + /** + * Create a new java.nio.ByteBuffer on the underlying storage, such that + * positioning this buffer to a particular byte using {@link #byteIndex(int)} or + * {@link #byteIndex(int[])} positions it at the first byte of the item so indexed. + */ + abstract protected ByteBuffer getNIOByteBufferImpl(); @Override public ByteBuffer getNIOByteBuffer() { - // The buffer spans the whole storage, which may include data not in the view - ByteBuffer b = ByteBuffer.wrap(storage); - b.limit(calcGreatestIndex() + getItemsize()).position(index0); - // Return as read-only if it is. - return isReadonly() ? b.asReadOnlyBuffer() : b; + // The buffer spans the whole storage + ByteBuffer b = getNIOByteBufferImpl(); + // For the one-dimensional contiguous case it makes sense to set the limit: + if (shape.length == 1) { + int stride = strides[0]; + if (getItemsize() == stride) { + b.limit(index0 + shape[0] * stride); + } + } + // The buffer is positioned at item[0] + b.position(index0); + return b; } @Override public ByteBuffer getNIOByteBuffer(int index) { // The buffer spans the whole storage but is positioned at index - ByteBuffer b = getNIOByteBuffer(); - b.position(calcIndex(index)); + ByteBuffer b = getNIOByteBufferImpl(); + b.position(byteIndex(index)); return b; } @Override public ByteBuffer getNIOByteBuffer(int... indices) { // The buffer spans the whole storage but is positioned at indices[] - ByteBuffer b = getNIOByteBuffer(); - b.position(calcIndex(indices)); - // Return as read-only if it is. + ByteBuffer b = getNIOByteBufferImpl(); + b.position(byteIndex(indices)); return b; } + @Override + public boolean hasArray() { + // AS_ARRAY is a non-navigation flag, so is inverted in gFeatureFlags + return (gFeatureFlags & AS_ARRAY) == 0; // i.e. featureFlags & AS_ARRAY is true + } + @SuppressWarnings("deprecation") @Override public Pointer getBuf() { - return new Pointer(storage, index0); + checkHasArray(); + return new Pointer(getNIOByteBuffer().array(), index0); } @SuppressWarnings("deprecation") @Override public Pointer getPointer(int index) throws IndexOutOfBoundsException { - return new Pointer(storage, calcIndex(index)); + Pointer p = getBuf(); + p.offset = byteIndex(index); + return p; } @SuppressWarnings("deprecation") @Override public Pointer getPointer(int... indices) throws IndexOutOfBoundsException { - return new Pointer(storage, calcIndex(indices)); + Pointer p = getBuf(); + p.offset = byteIndex(indices); + return p; } @Override @@ -654,10 +703,73 @@ return null; } + private boolean isCContiguous() { + /* + * If we were to compute the strides array for a C-contiguous array, the last stride would + * equal the item size, and generally stride[k-1] = shape[k]*stride[k]. This is the basis of + * the test. However, note that for any k where shape[k]==1 there is no "next sub-array" + * and no discontiguity. + */ + final int N = shape.length; + /* + * size is the stride in bytes-index from item[i0,i1,...,ik,0,...,0] to + * item[i0,i1,...,ik+1,0,...,0]. Start the iteration at the largest k. An increment of one + * in the last index makes a stride of the item size. + */ + int size = getItemsize(); + for (int k = N - 1; k >= 0; k--) { + int nk = shape[k]; + if (nk > 1) { + if (strides[k] != size) { + return false; + } + size *= nk; + } + } + return true; + } + + private boolean isFortranContiguous() { + /* + * If we were to compute the strides array for a Fortran-contiguous array, the first stride would + * equal the item size, and generally stride[k+1] = shape[k]*stride[k]. This is the basis of + * the test. However, note that for any k where shape[k]==1 there is no "next sub-array" + * and no discontiguity. + */ + final int N = shape.length; + /* + * size is the stride in bytes-index from item[0,...,0,ik,0,...,0] to + * item[0,...,0,ik+1,0,...,0]. Start the iteration at k=0. An increment of one + * in the first index makes a stride of the item size. + */ + int size = getItemsize(); + for (int k = 0; k < N; k++) { + int nk = shape[k]; + if (nk > 1) { + if (strides[k] != size) { + return false; + } + size *= nk; + } + } + return true; + } + @Override public boolean isContiguous(char order) { - // Correct for one-dimensional buffers - return true; + if (getSuboffsets() != null) { + return false; + } + switch (order) { + case 'C': + return isCContiguous(); + case 'F': + return isFortranContiguous(); + case 'A': + return isCContiguous() || isFortranContiguous(); + default: + return false; + } } @Override @@ -683,7 +795,7 @@ protected void releaseAction() {} /** - * Some PyBuffers, those created by slicing a PyBuffer are related to + * Some PyBuffers, those created by slicing a PyBuffer, are related to * a root PyBuffer. During creation of such a slice, we need to supply a value for * this root. If the present object is not itself a slice, this root is the object itself; if * the buffer is already a slice, it is the root it was given at creation time. Often this is @@ -697,6 +809,20 @@ } /** + * The toString() method of a buffer reproduces the values in the buffer (as unsigned integers) + * as the character codes of a String. + */ + @Override + public String toString() { + int n = getLen(); + StringBuilder sb = new StringBuilder(n); + for (int i = 0; i < n; i++) { + sb.appendCodePoint(intAt(i)); + } + return sb.toString(); + } + + /** * Check the number of indices (but not their values), raising a Python BufferError if this does * not match the number of dimensions. This is a helper for N-dimensional arrays. * @@ -728,17 +854,25 @@ } /** - * The toString() method of a buffer reproduces the values in the buffer (as unsigned integers) - * as the character codes of a String. + * Check that the buffer is writable. + * + * @throws PyException (TypeError) if not */ - @Override - public String toString() { - int n = getLen(); - StringBuilder sb = new StringBuilder(n); - for (int i = 0; i < n; i++) { - sb.appendCodePoint(intAt(i)); + protected void checkWritable() throws PyException { + if (isReadonly()) { + throw notWritable(); } - return sb.toString(); + } + + /** + * Check that the buffer is backed by an array the client can access as byte[]. + * + * @throws PyException (BufferError) if not + */ + protected void checkHasArray() throws PyException { + if (!hasArray()) { + throw bufferIsNot("accessible as a Java array"); + } } /** diff --git a/src/org/python/core/buffer/SimpleBuffer.java b/src/org/python/core/buffer/SimpleBuffer.java --- a/src/org/python/core/buffer/SimpleBuffer.java +++ b/src/org/python/core/buffer/SimpleBuffer.java @@ -1,7 +1,5 @@ package org.python.core.buffer; -import java.nio.ByteBuffer; - import org.python.core.PyBuffer; import org.python.core.PyException; import org.python.core.util.StringUtil; @@ -9,12 +7,7 @@ /** * Buffer API over a read-only one-dimensional array of one-byte items. */ -public class SimpleBuffer extends BaseBuffer { - - /** - * The strides array for this type is always a single element array with a 1 in it. - */ - protected static final int[] SIMPLE_STRIDES = {1}; +public class SimpleBuffer extends BaseArrayBuffer { /** * Provide an instance of SimpleBuffer with navigation variables partly @@ -27,7 +20,7 @@ super(CONTIGUITY | SIMPLE); // Initialise navigation shape = new int[1]; - strides = SIMPLE_STRIDES; + strides = CONTIG_STRIDES; // suboffsets is always null for this type. } @@ -54,8 +47,9 @@ ArrayIndexOutOfBoundsException { this(); this.storage = storage; // Exported data + // Initialise navigation this.index0 = index0; // Index to be treated as item[0] - this.shape[0] = size; // Number of items in exported data + shape[0] = size; // Number of items in exported data // Check arguments using the "all non-negative" trick if ((index0 | size | storage.length - (index0 + size)) < 0) { @@ -92,11 +86,8 @@ * @param storage the array of bytes storing the implementation of the exporting object * @throws NullPointerException if storage is null */ - // XXX: "for sub-class use" = should be protected? - public SimpleBuffer(byte[] storage) throws NullPointerException { - this(); - this.storage = storage; // Exported data (index0=0 from initialisation) - this.shape[0] = storage.length; // Number of units in whole array + protected SimpleBuffer(byte[] storage) throws NullPointerException { + this(storage, 0, storage.length); } /** @@ -114,11 +105,6 @@ checkRequestFlags(flags); // Check request is compatible with type } - @Override - public boolean isReadonly() { - return true; - } - /** * {@inheritDoc} *

@@ -134,72 +120,26 @@ /** * {@inheritDoc} *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. + * In SimpleBuffer the calculation is specialised for one dimension, no striding, + * and an item size of 1. */ @Override - public byte byteAt(int index) throws IndexOutOfBoundsException { - // Implement directly: a bit quicker than the default - return storage[index0 + index]; - } - - /** - * {@inheritDoc} - *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. - */ - @Override - public int intAt(int index) throws IndexOutOfBoundsException { - // Implement directly: a bit quicker than the default - return 0xff & storage[index0 + index]; + protected int byteIndex(int index) throws IndexOutOfBoundsException { + if (index < 0 || index >= shape[0]) { + throw new IndexOutOfBoundsException(); + } + return index0 + index; } @Override - protected int calcIndex(int index) throws IndexOutOfBoundsException { - return index0 + index; - } - - /** - * {@inheritDoc} - *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. - */ - @Override - public byte byteAt(int... indices) throws IndexOutOfBoundsException { - checkDimension(indices.length); - return byteAt(indices[0]); - } - - @Override - protected int calcIndex(int... indices) throws IndexOutOfBoundsException { - // BaseBuffer implementation can be simplified since if indices.length!=1 we error. - checkDimension(indices.length); // throws if != 1 - return calcIndex(indices[0]); - } - - /** - * {@inheritDoc} - *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. - */ - @Override - public void copyTo(int srcIndex, byte[] dest, int destPos, int length) - throws IndexOutOfBoundsException { - System.arraycopy(storage, index0 + srcIndex, dest, destPos, length); - } - - @Override - public PyBuffer getBufferSlice(int flags, int start, int length) { - if (length > 0) { + public PyBuffer getBufferSlice(int flags, int start, int count) { + if (count > 0) { // Translate relative to underlying buffer int compIndex0 = index0 + start; // Create the slice from the sub-range of the buffer - return new SimpleView(getRoot(), flags, storage, compIndex0, length); + return new SimpleView(getRoot(), flags, storage, compIndex0, count); } else { - // Special case for length==0 where above logic would fail. Efficient too. + // Special case for count==0 where above logic would fail. Efficient too. return new ZeroByteBuffer.View(getRoot(), flags); } } @@ -209,22 +149,22 @@ *

* SimpleBuffer provides an implementation for slicing contiguous bytes in one * dimension. In that case, x(i) = u(r+i) for i = 0..L-1 where u is the underlying - * buffer, and r and L are the start and length with which x was created + * buffer, and r and L are the start and count with which x was created * from u. Thus y(k) = u(r+s+km), that is, the composite offset is r+s and * the stride is m. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { - if (stride == 1 || length < 2) { + if (stride == 1 || count < 2) { // Unstrided slice of simple buffer is itself simple - return getBufferSlice(flags, start, length); + return getBufferSlice(flags, start, count); } else { // Translate relative to underlying buffer int compIndex0 = index0 + start; // Construct a view, taking a lock on the root object (this or this.root) - return new Strided1DBuffer.SlicedView(getRoot(), flags, storage, compIndex0, length, + return new Strided1DBuffer.SlicedView(getRoot(), flags, storage, compIndex0, count, stride); } } diff --git a/src/org/python/core/buffer/SimpleStringBuffer.java b/src/org/python/core/buffer/SimpleStringBuffer.java --- a/src/org/python/core/buffer/SimpleStringBuffer.java +++ b/src/org/python/core/buffer/SimpleStringBuffer.java @@ -30,6 +30,11 @@ * @param flags consumer requirements */ public SimpleStringBuffer(int flags, String bufString) { + /* + * Leaving storage=null is ok because we carefully override every method that uses it, + * deferring creation of the storage byte array until we absolutely must have one. + */ + super(); // Save the backing string this.bufString = bufString; shape[0] = bufString.length(); @@ -54,32 +59,31 @@ * This method uses {@link String#charAt(int)} rather than create an actual byte buffer. */ @Override - public byte byteAt(int index) throws IndexOutOfBoundsException { - // Avoid creating buf by using String.charAt + public final byte byteAtImpl(int index) { return (byte)bufString.charAt(index); } /** * {@inheritDoc} *

+ * In SimpleStringBuffer we can simply return the argument. + */ + @Override + protected final int byteIndex(int index) { + // We do not check the index because String will do it for us. + return index; + } + + /** + * {@inheritDoc} + *

* This method uses {@link String#charAt(int)} rather than create an actual byte buffer. */ @Override - public int intAt(int index) throws IndexOutOfBoundsException { - // Avoid creating buf by using String.charAt - return bufString.charAt(index); - } - - /** - * {@inheritDoc} - *

- * This method uses {@link String#charAt(int)} rather than create an actual byte buffer. - */ - @Override - public void copyTo(int srcIndex, byte[] dest, int destPos, int length) + public void copyTo(int srcIndex, byte[] dest, int destPos, int count) throws IndexOutOfBoundsException { // Avoid creating buf by using String.charAt - int endIndex = srcIndex + length, p = destPos; + int endIndex = srcIndex + count, p = destPos; for (int i = srcIndex; i < endIndex; i++) { dest[p++] = (byte)bufString.charAt(i); } @@ -91,13 +95,13 @@ * The SimpleStringBuffer implementation avoids creation of a byte buffer. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length) { - if (length > 0) { - // The new string content is just a sub-string. (Non-copy operation in Java.) + public PyBuffer getBufferSlice(int flags, int start, int count) { + if (count > 0) { + // The new string content is just a sub-string. return new SimpleStringView(getRoot(), flags, - bufString.substring(start, start + length)); + bufString.substring(start, start + count)); } else { - // Special case for length==0 where start out of bounds sometimes raises exception. + // Special case for count==0 where start out of bounds sometimes raises exception. return new ZeroByteBuffer.View(getRoot(), flags); } } @@ -108,23 +112,26 @@ * The SimpleStringBuffer implementation creates an actual byte buffer. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { if (stride == 1) { // Unstrided slice of a SimpleStringBuffer is itself a SimpleStringBuffer. - return getBufferSlice(flags, start, length); + return getBufferSlice(flags, start, count); } else { // Force creation of the actual byte array from the String. ensureHaveBytes(); // Now we are effectively a SimpleBuffer, return the strided view. - return super.getBufferSlice(flags, start, length, stride); + return super.getBufferSlice(flags, start, count, stride); } } @Override - public ByteBuffer getNIOByteBuffer() { + protected ByteBuffer getNIOByteBufferImpl() { // Force creation of the actual byte array from the String. ensureHaveBytes(); - return super.getNIOByteBuffer(); + // The buffer spans the whole storage, which may include data not in the view + ByteBuffer b = ByteBuffer.wrap(storage); + // Return as read-only. + return b.asReadOnlyBuffer(); } /** 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 @@ -36,64 +36,28 @@ * @throws PyException (BufferError) when expectations do not correspond with the type */ public SimpleWritableBuffer(int flags, byte[] storage) throws PyException, NullPointerException { - super(storage); // Construct SimpleBuffer on whole array - addFeatureFlags(WRITABLE); - checkRequestFlags(flags); // Check request is compatible with type - } - - @Override - public boolean isReadonly() { - return false; + this(flags, storage, 0, storage.length); } /** * {@inheritDoc} *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. + * Declared final returning true in SimpleWritableBuffer + * to make checks unnecessary. */ @Override - public void storeAt(byte value, int index) { - // Implement directly and don't ask whether read-only - storage[index0 + index] = value; + public final boolean isReadonly() { + return false; } - /** - * {@inheritDoc} - *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. - */ + /** Do nothing: the buffer is writable. */ @Override - public void storeAt(byte value, int... indices) { - checkDimension(indices.length); - storeAt(value, indices[0]); - } + protected final void checkWritable() {} - /** - * {@inheritDoc} - *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. - */ @Override - public void copyFrom(byte[] src, int srcPos, int destIndex, int length) { - System.arraycopy(src, srcPos, storage, index0 + destIndex, length); - } - - /** - * {@inheritDoc} - *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. - */ - @Override - public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { - if (src.getLen() != getLen()) { - throw differentStructure(); - } - // Get the source to deliver efficiently to our byte storage - src.copyTo(storage, index0); + protected void storeAtImpl(byte value, int byteIndex) { + // Implement directly and don't ask whether read-only + storage[byteIndex] = value; } /** @@ -103,14 +67,14 @@ * writable. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length) { - if (length > 0) { + public PyBuffer getBufferSlice(int flags, int start, int count) { + if (count > 0) { // Translate relative to underlying buffer int compIndex0 = index0 + start; // Create the slice from the sub-range of the buffer - return new SimpleView(getRoot(), flags, storage, compIndex0, length); + return new SimpleView(getRoot(), flags, storage, compIndex0, count); } else { - // Special case for length==0 where above logic would fail. Efficient too. + // Special case for count==0 where above logic would fail. Efficient too. return new ZeroByteBuffer.View(getRoot(), flags); } } @@ -122,18 +86,18 @@ * writable. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { - if (stride == 1 || length < 2) { + if (stride == 1 || count < 2) { // Unstrided slice of simple buffer is itself simple - return getBufferSlice(flags, start, length); + return getBufferSlice(flags, start, count); } else { // Translate relative to underlying buffer int compIndex0 = index0 + start; // Construct a view, taking a lock on the root object (this or this.root) return new Strided1DWritableBuffer.SlicedView(getRoot(), flags, storage, compIndex0, - length, stride); + count, stride); } } 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 @@ -5,7 +5,7 @@ /** * Read-only buffer API over a one-dimensional array of one-byte items, that are evenly-spaced in a - * storage array. The buffer has storage, index0 and length + * storage array. The buffer has storage, index0 and count * properties in the usual way, designating a slice (or all) of a byte array, but also a * stride property (equal to getStrides()[0]). *

@@ -14,7 +14,7 @@ * Designate by x(j), for j=0..L-1, the byte at index j, that is, the byte * retrieved by x.byteAt(j). Thus, we store x(j) at u(a+pj), that is, * x(0) = u(a). When we construct such a buffer, we have to supply a = - * index0, L = length, and p = stride as the + * index0, L = count, and p = stride as the * constructor arguments. The last item in the slice x(L-1) is stored at u(a+p(L-1)). * For the simple case of positive stride, constructor argument index0 is the low index * of the range occupied by the data. When the stride is negative, that is to say p<0, and @@ -28,7 +28,7 @@ * create the memoryview that is returned as an extended slice of a * memoryview. */ -public class Strided1DBuffer extends BaseBuffer { +public class Strided1DBuffer extends BaseArrayBuffer { /** * Step size in the underlying buffer essential to correct translation of an index (or indices) @@ -38,30 +38,6 @@ protected int stride; /** - * Provide an instance of Strided1DBuffer with navigation variables partly - * initialised, for sub-class use. To complete initialisation, the sub-class normally must - * assign the navigational properties and call {@link #checkRequestFlags(int)} passing the - * consumer's request flags. - * - *

-     * this.storage = storage;          // Exported data
-     * this.index0 = index0;            // Index to be treated as item[0]
-     * this.shape[0] = length;          // Number of items in exported data
-     * this.stride = stride;            // Between items
-     * 
- * - * The pre-defined {@link #strides} field remains null until {@link #getStrides} is - * called. - */ - protected Strided1DBuffer() { - super(STRIDES); - // Initialise navigation - shape = new int[1]; - // strides is created on demand; - // suboffsets is always null for this type. - } - - /** * Provide an instance of Strided1DBuffer with navigation variables initialised, * for sub-class use. The buffer ({@link #storage}, {@link #index0}), and the navigation ( * {@link #shape} array and {@link #stride}) will be initialised from the arguments (which are @@ -69,27 +45,27 @@ *

* The sub-class constructor should check that the intended access is compatible with this * object by calling {@link #checkRequestFlags(int)}. (See the source of - * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, byte[], int, int, int)} - * for an example of this use.) + * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, byte[], int, int, int)} for an + * example of this use.) * * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] - * @param length number of items in the slice + * @param count number of items in the slice * @param stride in between successive elements of the new PyBuffer * @throws NullPointerException if storage is null - * @throws ArrayIndexOutOfBoundsException if index0, length and + * @throws ArrayIndexOutOfBoundsException if index0, count and * stride are inconsistent with storage.length */ - // XXX: "for sub-class use" = should be protected? - public Strided1DBuffer(byte[] storage, int index0, int length, int stride) + protected Strided1DBuffer(byte[] storage, int index0, int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException { - this(); + super(STRIDES); this.storage = storage; // Exported data this.index0 = index0; // Index to be treated as item[0] - this.shape[0] = length; // Number of items in exported data + this.shape = new int[] {count}; // Number of items in exported data this.stride = stride; // Between items + this.strides = new int[] {stride}; - if (length == 0) { + if (count == 0) { // Nothing to check as we'll make no accesses addFeatureFlags(CONTIGUITY); @@ -99,20 +75,20 @@ if (stride == 1) { lo = index0; // First byte of item[0] - hi = index0 + length; // Last byte of item[L-1] + 1 + hi = index0 + count; // Last byte of item[L-1] + 1 addFeatureFlags(CONTIGUITY); } else if (stride > 1) { lo = index0; // First byte of item[0] - hi = index0 + (length - 1) * stride + 1; // Last byte of item[L-1] + 1 + hi = index0 + (count - 1) * stride + 1; // Last byte of item[L-1] + 1 } else { hi = index0 + 1; // Last byte of item[0] + 1 - lo = index0 + (length - 1) * stride; // First byte of item[L-1] + lo = index0 + (count - 1) * stride; // First byte of item[L-1] } // Check indices using "all non-negative" trick - if ((length | lo | (storage.length - lo) | hi | (storage.length - hi)) < 0) { + if ((count | lo | (storage.length - lo) | hi | (storage.length - hi)) < 0) { throw new ArrayIndexOutOfBoundsException(); } } @@ -122,31 +98,31 @@ * Provide an instance of Strided1DBuffer on a particular array of bytes specifying * a starting index, the number of items in the result, and a byte-indexing stride. The result * of byteAt(i) will be equal to storage[index0+stride*i] (whatever - * the sign of stride), valid for 0<=i<length. The constructor + * the sign of stride), valid for 0<=i<count. The constructor * checks that all these indices lie within the storage array (unless - * length=0). + * count=0). *

* The constructed PyBuffer meets the consumer's expectations as expressed in the * flags argument, or an exception will be thrown if these are incompatible with * the type (e.g. the consumer does not specify that it understands the strides array). Note * that the actual range in the storage array, the lowest and highest index, is not - * explicitly passed, but is implicit in index0, length and + * explicitly passed, but is implicit in index0, count and * stride. The constructor checks that these indices lie within the - * storage array (unless length=0). + * storage array (unless count=0). * * @param flags consumer requirements * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] - * @param length number of items in the slice + * @param count number of items in the slice * @param stride in between successive elements of the new PyBuffer * @throws NullPointerException if storage is null - * @throws ArrayIndexOutOfBoundsException if index0, length and + * @throws ArrayIndexOutOfBoundsException if index0, count and * stride are inconsistent with storage.length * @throws PyException (BufferError) when expectations do not correspond with the type */ - public Strided1DBuffer(int flags, byte[] storage, int index0, int length, int stride) + public Strided1DBuffer(int flags, byte[] storage, int index0, int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException, PyException { - this(storage, index0, length, stride); + this(storage, index0, count, stride); checkRequestFlags(flags); // Check request is compatible with type } @@ -157,69 +133,35 @@ } @Override - public byte byteAt(int index) throws IndexOutOfBoundsException { - return storage[index0 + index * stride]; - } - - @Override - protected int calcIndex(int index) throws IndexOutOfBoundsException { + protected final int byteIndex(int index) throws IndexOutOfBoundsException { + if (index < 0 || index >= shape[0]) { + throw new IndexOutOfBoundsException(); + } return index0 + index * stride; } - @Override - protected int calcIndex(int... indices) throws IndexOutOfBoundsException { - // BaseBuffer implementation can be simplified since if indices.length!=1 we error. - checkDimension(indices.length); // throws if != 1 - return calcIndex(indices[0]); - } - - /** - * {@inheritDoc} Strided1DBuffer provides a version optimised for strided bytes in - * one dimension. - */ - @Override - public void copyTo(int srcIndex, byte[] dest, int destPos, int length) - throws IndexOutOfBoundsException { - // Data is here in the buffers - int s = index0 + srcIndex * stride; - int d = destPos; - - // Strategy depends on whether items are laid end-to-end contiguously or there are gaps - if (stride == 1) { - // stride == itemsize: straight copy of contiguous bytes - System.arraycopy(storage, s, dest, d, length); - - } else { - // Non-contiguous copy: single byte items - int limit = s + length * stride; - for (; s != limit; s += stride) { - dest[d++] = storage[s]; - } - } - } - /** * {@inheritDoc} *

* Strided1DBuffer provides an implementation for slicing already-strided bytes in * one dimension. In that case, x(i) = u(r+ip) for i = 0..L-1 where u is the - * underlying buffer, and r, p and L are the start, stride and length with + * underlying buffer, and r, p and L are the start, stride and count with * which x was created from u. Thus y(k) = u(r+sp+kmp), that is, the * composite index0 is r+sp and the composite stride is * mp. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { - if (length > 0) { + if (count > 0) { // Translate start relative to underlying buffer int compStride = this.stride * stride; int compIndex0 = index0 + start * this.stride; // Construct a view, taking a lock on the root object (this or this.root) - return new SlicedView(getRoot(), flags, storage, compIndex0, length, compStride); + return new SlicedView(getRoot(), flags, storage, compIndex0, count, compStride); } else { - // Special case for length==0 where above logic would fail. Efficient too. + // Special case for count==0 where above logic would fail. Efficient too. return new ZeroByteBuffer.View(getRoot(), flags); } } @@ -238,15 +180,6 @@ return getPointer(indices[0]); } - @Override - public int[] getStrides() { - if (strides == null) { - strides = new int[1]; - strides[0] = stride; - } - return strides; - } - /** * A Strided1DBuffer.SlicedView represents a non-contiguous subsequence of a simple * buffer. @@ -282,6 +215,7 @@ @Override public void releaseAction() { + // XXX Consider making this automatic within BaseBuffer.release() when getRoot()!=this // We have to release the root too if ours was final. root.release(); } 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 @@ -14,67 +14,52 @@ * Provide an instance of Strided1DWritableBuffer on a particular array of bytes * specifying a starting index, the number of items in the result, and a byte-indexing stride. * The result of byteAt(i) will be equal to storage[index0+stride*i] - * (whatever the sign of stride>0), valid for 0<=i<length. + * (whatever the sign of stride>0), valid for 0<=i<count. *

* The constructed PyBuffer meets the consumer's expectations as expressed in the * flags argument, or an exception will be thrown if these are incompatible with * the type (e.g. the consumer does not specify that it understands the strides array). Note * that the actual range in the storage array, the lowest and highest index, is not - * explicitly passed, but is implicit in index0, length and + * explicitly passed, but is implicit in index0, count and * stride. The caller is responsible for checking these fall within the array, or * the sub-range the caller is allowed to use. * * @param flags consumer requirements * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] - * @param length number of items in the slice + * @param count number of items in the slice * @param stride in between successive elements of the new PyBuffer * @throws NullPointerException if storage is null - * @throws ArrayIndexOutOfBoundsException if index0, length and + * @throws ArrayIndexOutOfBoundsException if index0, count and * stride are inconsistent with storage.length * @throws PyException (BufferError) when expectations do not correspond with the type */ - public Strided1DWritableBuffer(int flags, byte[] storage, int index0, int length, int stride) + public Strided1DWritableBuffer(int flags, byte[] storage, int index0, int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException, PyException { - super(storage, index0, length, stride); + super(storage, index0, count, stride); addFeatureFlags(WRITABLE); checkRequestFlags(flags); // Check request is compatible with type } + /** + * {@inheritDoc} + *

+ * Declared final returning true in + * Strided1DWritableBuffer to make checks unnecessary. + */ @Override - public boolean isReadonly() { + public final boolean isReadonly() { return false; } + /** Do nothing: the buffer is writable. */ @Override - public void storeAt(byte value, int index) throws IndexOutOfBoundsException, PyException { - storage[index0 + index * stride] = value; - } + protected final void checkWritable() {} - /** - * {@inheritDoc} Strided1DWritableBuffer provides a version optimised for strided - * bytes in one dimension. - */ @Override - public void copyFrom(byte[] src, int srcPos, int destIndex, int length) - throws IndexOutOfBoundsException, PyException { - - // Data is here in the buffers - int s = srcPos; - int d = index0 + destIndex * stride; - - // Strategy depends on whether items are laid end-to-end or there are gaps - if (stride == 1) { - // Straight copy of contiguous bytes - System.arraycopy(src, srcPos, storage, d, length); - - } else { - // Non-contiguous copy: single byte items - int limit = d + length * stride; - for (; d != limit; d += stride) { - storage[d] = src[s++]; - } - } + protected void storeAtImpl(byte value, int byteIndex) { + // Implement directly and don't ask whether read-only + storage[byteIndex] = value; } /** @@ -84,17 +69,17 @@ * slice. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { - if (length > 0) { + if (count > 0) { // Translate start relative to underlying buffer int compStride = this.stride * stride; int compIndex0 = index0 + start * this.stride; // Construct a view, taking a lock on the root object (this or this.root) - return new SlicedView(getRoot(), flags, storage, compIndex0, length, compStride); + return new SlicedView(getRoot(), flags, storage, compIndex0, count, compStride); } else { - // Special case for length==0 where above logic would fail. Efficient too. + // Special case for count==0 where above logic would fail. Efficient too. return new ZeroByteBuffer.View(getRoot(), flags); } } diff --git a/src/org/python/core/buffer/ZeroByteBuffer.java b/src/org/python/core/buffer/ZeroByteBuffer.java --- a/src/org/python/core/buffer/ZeroByteBuffer.java +++ b/src/org/python/core/buffer/ZeroByteBuffer.java @@ -12,7 +12,7 @@ * operations like {@link #copyTo(byte[], int)}) and {@link #toString()} efficiently do nothing, * instead of calling complicated logic that finally does nothing. */ -public class ZeroByteBuffer extends BaseBuffer { +public class ZeroByteBuffer extends BaseArrayBuffer { /** Shared instance of a zero-length storage. */ private static final byte[] EMPTY = new byte[0]; @@ -22,19 +22,25 @@ /** * Construct an instance of a zero-length buffer, choosing whether it should report itself to be - * read-only through {@link #isReadonly()}. This is moot, as any attempt to write to it produces - * an {@link IndexOutOfBoundsException}, but it is less surprising for client code that may ask, - * if the readability follows that of the object from which the buffer is derived. + * read-only through {@link #isReadonly()} or as having a backing array through + * {@link #hasArray()}. These properties are moot, as any attempt to write to the pretended + * backing array produces an {@link IndexOutOfBoundsException}, but it is less surprising for + * client code that may ask, if the results are customary for the exporting object. * * @param flags consumer requirements - * @param readonly set true if readonly - * @throws PyException (BufferError) when expectations do not correspond with the type + * @param readonly set true if not to be considered writable + * @param hasArray set true if to be considered as backed by an array + * @throws PyException (BufferError) when client expectations do not correspond with the type */ - public ZeroByteBuffer(int flags, boolean readonly) throws PyException { - super(CONTIGUITY | SIMPLE | (readonly ? 0 : WRITABLE)); + public ZeroByteBuffer(int flags, boolean readonly, boolean hasArray) throws PyException { + super(CONTIGUITY | (readonly ? 0 : WRITABLE)); this.storage = EMPTY; // Empty array this.shape = SHAPE; // {0} - this.strides = SimpleBuffer.SIMPLE_STRIDES; // {1} + this.strides = BaseBuffer.CONTIG_STRIDES; // {1} + if (!hasArray) { + // super() knows we have an array, but this truth is inconvenient here. + removeFeatureFlags(AS_ARRAY); + } checkRequestFlags(flags); } @@ -47,7 +53,7 @@ * In a ZeroByteBuffer, the index is always out of bounds. */ @Override - protected int calcIndex(int index) throws IndexOutOfBoundsException { + protected int byteIndex(int index) throws IndexOutOfBoundsException { // This causes all access to the bytes in to throw (since BaseBuffer calls it). throw new IndexOutOfBoundsException(); } @@ -56,7 +62,7 @@ * In a ZeroByteBuffer, if the dimensions are right, the index is out of bounds anyway. */ @Override - protected int calcIndex(int... indices) throws IndexOutOfBoundsException { + protected int byteIndex(int... indices) throws IndexOutOfBoundsException { // Bootless dimension check takes precedence (for consistency with other buffers) checkDimension(indices); // This causes all access to the bytes to throw (since BaseBuffer calls it). @@ -79,28 +85,28 @@ * In a ZeroByteBuffer, there is simply nothing to copy. */ @Override - public void copyTo(int srcIndex, byte[] dest, int destPos, int length) + public void copyTo(int srcIndex, byte[] dest, int destPos, int count) throws IndexOutOfBoundsException, PyException { // Nothing to copy } /** - * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source length - * is zero. + * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source count is + * zero. */ @Override - public void copyFrom(byte[] src, int srcPos, int destIndex, int length) + public void copyFrom(byte[] src, int srcPos, int destIndex, int count) throws IndexOutOfBoundsException, PyException { if (this.isReadonly()) { throw notWritable(); - } else if (length > 0) { + } else if (count > 0) { throw new IndexOutOfBoundsException(); } } /** - * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source length - * is zero. + * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source count is + * zero. */ @Override public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { @@ -116,8 +122,8 @@ * as a result, with the export count incremented. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length) { - if (start == 0 && length <= 0) { + public PyBuffer getBufferSlice(int flags, int start, int count) { + if (start == 0 && count <= 0) { return this.getBuffer(flags); } else { throw new IndexOutOfBoundsException(); @@ -129,9 +135,9 @@ * as a result, with the export count incremented. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { - // It can't matter what the stride is since length is zero, or there's an error. - return getBufferSlice(flags, start, length); + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { + // It can't matter what the stride is since count is zero, or there's an error. + return getBufferSlice(flags, start, count); } /** @@ -175,7 +181,7 @@ */ public View(PyBuffer root, int flags) { // Create a new ZeroByteBuffer on who-cares-what byte array - super(flags, root.isReadonly()); + super(flags, root.isReadonly(), root.hasArray()); // But we still have to get a lease on the root PyBuffer this.root = root.getBuffer(FULL_RO); } 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 @@ -71,13 +71,13 @@ protected static final int[] sliceSteps = {1, 2, 3, 7}; /** Exception raising requires the Jython interpreter to be initialised **/ - protected PythonInterpreter interp = new PythonInterpreter(); + protected static PythonInterpreter interp = new PythonInterpreter(); /** The test material and a buffer created by the test-runner. */ - private TestSpec spec; - ByteMaterial ref; - BufferProtocol obj; - PyBuffer view; + protected TestSpec spec; + protected ByteMaterial ref; + protected BufferProtocol obj; + protected PyBuffer view; /** * Construct an instance to run one test, using one set of test data. @@ -103,11 +103,11 @@ /* * Values for initialising the exporters. */ - private static final ByteMaterial byteMaterial = new ByteMaterial(10, 16, 3); - 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, LONG, 5); + protected static final ByteMaterial byteMaterial = new ByteMaterial(10, 16, 3); + protected static final ByteMaterial abcMaterial = new ByteMaterial("abcdefgh"); + protected static final ByteMaterial stringMaterial = new ByteMaterial("Mon c?t? f?cheux"); + protected static final ByteMaterial emptyMaterial = new ByteMaterial(new byte[0]); + protected static final ByteMaterial longMaterial = new ByteMaterial(0, LONG, 5); /** * Generate test data to be held in the testing framework and used to construct tests. This @@ -161,6 +161,19 @@ }; s.add(stringExporter, stringMaterial); + // Tests with an buffer implementation directly extending BaseBuffer + + ExporterFactory rollYourOwnExporter = new WritableExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new RollYourOwnExporter(m.getBytes()); + } + + }; + s.add(rollYourOwnExporter, byteMaterial); + s.add(rollYourOwnExporter, emptyMaterial); + // Tests with PyByteArray ExporterFactory pyByteArrayExporter = new WritableExporterFactory() { @@ -990,10 +1003,12 @@ @Test public void testIsContiguous() { announce("isContiguous"); - // True for all test material and orders (since 1-dimensional) + // All test material is 1-dimensional so it's fairly simple and same for all orders + int ndim = spec.shape[0], stride = spec.getStride(), itemsize = spec.getItemsize(); + boolean contig = ndim < 2 || stride == itemsize; for (String orderMsg : validOrders) { char order = orderMsg.charAt(0); - assertTrue(orderMsg, view.isContiguous(order)); + assertEquals(orderMsg, view.isContiguous(order), contig); } } @@ -1069,17 +1084,17 @@ } /* - * ------------------------------------------------------------------------------------------- A - * series of custom exporters to permit testing abstracted from the Jython interpreter. These + * -------------------------------------------------------------------------------------------- + * A series of custom exporters to permit testing abstracted from the Jython interpreter. These * use the implementation classes in org.python.core.buffer in ways very similar to the * implementations of bytearray and str. - * ------------------------------------------------------------------------------------------- + * -------------------------------------------------------------------------------------------- */ /** - * A class to act as an exporter that uses the SimpleReadonlyBuffer. The exporter exports a new - * PyBuffer object to each consumer (although each references the same internal storage) and it - * does not track their fate. You are most likely to use this approach with an exporting object - * that is immutable (or at least fixed in size). + * A class to act as an exporter that uses the SimpleBuffer. The exporter exports a new PyBuffer + * object to each consumer (although each references the same internal storage) and it does not + * track their fate. You are most likely to use this approach with an exporting object that is + * immutable (or at least fixed in size). */ static class SimpleExporter implements BufferProtocol { @@ -1109,7 +1124,8 @@ protected Reference export; /** - * Try to re-use existing exported buffer, or return null if can't. + * Try to re-use existing exported buffer, or return null if can't: modelled after the + * buffer re-use strategy in {@link PyByteArray}. */ protected BaseBuffer getExistingBuffer(int flags) { BaseBuffer pybuf = null; @@ -1191,7 +1207,7 @@ * avoiding the cost of duplicate buffers. This is the case with PyByteArray, which prohibits * operations that would resize it, while there are outstanding exports. */ - static class SimpleWritableExporter extends TestableExporter { + private static class SimpleWritableExporter extends TestableExporter { protected byte[] storage; @@ -1226,4 +1242,131 @@ } + /** A class to act as an exporter that uses the RollYourOwnBuffer. */ + private static class RollYourOwnExporter extends TestableExporter { + + protected byte[] storage; + + public RollYourOwnExporter(byte[] storage) { + this.storage = storage; + } + + @Override + public PyBuffer getBuffer(int flags) { + // If we have already exported a buffer it may still be available for re-use + BaseBuffer pybuf = getExistingBuffer(flags); + if (pybuf == null) { + // No existing export we can re-use + pybuf = new RollYourOwnArrayBuffer(flags, storage); + // Hold a reference for possible re-use + export = new WeakReference(pybuf); + } + return pybuf; + } + + } + + /** + * Minimal extension of BaseBuffer in order to test the default implementations there. They're + * slow, so mostly we override them in the implementations BaseArrayBuffer and BaseNIOBuffer, + * but they still have to be correct. The class represents a one-dimensional, strided array of + * bytes, so it can represent a slice of itself. + */ + private static class RollYourOwnArrayBuffer extends BaseBuffer { + + final static int FEATURES = PyBUF.WRITABLE | PyBUF.AS_ARRAY; + + final byte[] storage; + final PyBuffer root; + + /** + * Create a buffer view of the entire array. + * + * @param flags consumer requirements + * @param storage byte array exported in its entirety + */ + public RollYourOwnArrayBuffer(int flags, byte[] storage) { + this(null /* =this */, flags, storage, 0, storage.length, 1); + } + + /** + * Construct a slice of a one-dimensional byte buffer. + * + * @param root on which release must be called when this is released + * @param flags consumer requirements + * @param storage raw byte array containing exported data + * @param index0 index into storage of item[0] + * @param length number of items in the slice + * @param stride in between successive elements of the new PyBuffer + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public RollYourOwnArrayBuffer(PyBuffer root, int flags, byte[] storage, int index0, + int length, int stride) throws ArrayIndexOutOfBoundsException, + NullPointerException, PyException { + // Client will need to navigate using shape and strides if this is a slice + super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : PyBUF.STRIDES)); + this.storage = storage; + this.index0 = index0; + shape = new int[] {length}; + strides = new int[] {stride}; + // Check the potential index range + if (length > 0) { + int end = index0 + (length - 1) * stride; + final int END = storage.length - 1; + if (index0 < 0 || index0 > END || end < 0 || end > END) { + throw new IndexOutOfBoundsException(); + } + } + // Check client is compatible + checkRequestFlags(flags); + // Get a lease on the root PyBuffer (read-only). Last in case a check above fails. + if (root == null) { + this.root = this; + } else { + this.root = root.getBuffer(FULL_RO); + } + } + + @Override + protected PyBuffer getRoot() { + return root; + } + + @Override + public void releaseAction() { + // XXX Consider making this automatic within BaseBuffer.release() when getRoot()!=this + /* + * ... so that {@link #release()} takes care of this: sub-classes should not propagate + * the release themselves when overriding {@link #releaseAction()}. + */ + // We have to release the root too if ours was final and we are not that root. + if (root != this) { + root.release(); + } + } + + @Override + public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + int newStart = index0 + start * strides[0]; + int newStride = strides[0] * stride; + return new RollYourOwnArrayBuffer(root, flags, storage, newStart, length, newStride); + } + + @Override + public ByteBuffer getNIOByteBufferImpl() { + return ByteBuffer.wrap(storage); + } + + @Override + protected byte byteAtImpl(int byteIndex) { + return storage[byteIndex]; + } + + @Override + protected void storeAtImpl(byte value, int byteIndex) throws IndexOutOfBoundsException, + PyException { + storage[byteIndex] = value; + } + + } } diff --git a/tests/java/org/python/core/PyBufferTestSupport.java b/tests/java/org/python/core/PyBufferTestSupport.java --- a/tests/java/org/python/core/PyBufferTestSupport.java +++ b/tests/java/org/python/core/PyBufferTestSupport.java @@ -260,7 +260,7 @@ /** Either {@link PyBUF#FULL_RO} or {@link PyBUF#FULL} according to {@link #readonly}. */ final int flags; - /** Allowable basic flag combinations, such as */ + /** Allowable basic flag combinations, such as {@link PyBUF#STRIDES}. */ final int[] validFlags; /** Allowable additional flag combinations, such as {@link PyBUF#FORMAT} */ -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:25 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:25 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Add_PyBuffer=2EbyteIndex_me?= =?utf-8?q?thods=2E?= Message-ID: <20160827131225.45636.42843.8DD0C532@psf.io> https://hg.python.org/jython/rev/1cb1ab1e53f3 changeset: 7946:1cb1ab1e53f3 user: Jeff Allen date: Thu Jun 30 10:02:08 2016 +0100 summary: Add PyBuffer.byteIndex methods. Allow buffers to provide their indexing polynomial. This makes unnecessary methods returning a Pointer or ByteBuffer ready-positioned at a particular index. Minor impacts throughout the buffer implementations. files: src/org/python/core/PyBuffer.java | 55 +++++---- src/org/python/core/buffer/BaseArrayBuffer.java | 2 +- src/org/python/core/buffer/BaseBuffer.java | 53 +------- src/org/python/core/buffer/BaseNIOBuffer.java | 15 +- src/org/python/core/buffer/SimpleBuffer.java | 2 +- src/org/python/core/buffer/SimpleNIOBuffer.java | 2 +- src/org/python/core/buffer/SimpleStringBuffer.java | 5 +- src/org/python/core/buffer/Strided1DBuffer.java | 2 +- src/org/python/core/buffer/Strided1DNIOBuffer.java | 2 +- src/org/python/core/buffer/ZeroByteBuffer.java | 4 +- tests/java/org/python/core/PyBufferTest.java | 53 --------- 11 files changed, 56 insertions(+), 139 deletions(-) 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 @@ -238,10 +238,39 @@ */ public PyBuffer getBufferSlice(int flags, int start, int count, int stride); - // java.nio access to actual storage + // Access to underlying byte-oriented storage // /** + * Convert an item index (for a one-dimensional buffer) to an absolute byte index in the storage + * shared by the exporter. The storage exported as a PyBuffer is a linearly-indexed + * sequence of bytes, although it may not actually be a heap-allocated Java byte[] + * object. The purpose of this method is to allow the exporter to define the relationship + * between the item index (as used in {@link #byteAt(int)}) and the byte-index (as used with the + * ByteBuffer returned by {@link #getNIOByteBuffer()}). See + * {@link #byteIndex(int[])} for discussion of the multi-dimensional case. + * + * @param index item-index from consumer + * @return corresponding byte-index in actual storage + */ + // Should it throw IndexOutOfBoundsException if the index <0 or ≥shape[0]PyBuffer is a linearly-indexed sequence of + * bytes, although it may not actually be a heap-allocated Java byte[] object. The + * purpose of this method is to allow the exporter to define the relationship between the item + * index (as used in {@link #byteAt(int...)} and the byte-index (as used with the + * ByteBuffer returned by {@link #getNIOByteBuffer()}). + * + * @param indices n-dimensional item-index from consumer + * @return corresponding byte-index in actual storage + */ + // Should it throw IndexOutOfBoundsException if any index <0 or ≥shape[i]? + int byteIndex(int... indices); + + /** * Obtain a {@link java.nio.ByteBuffer} giving access to the bytes that hold the data being * exported by the original object. The position of the buffer is at the first byte of the item * with zero index (quite possibly not the lowest valid byte-index), the limit of the buffer is @@ -271,30 +300,6 @@ ByteBuffer getNIOByteBuffer(); /** - * Obtain a {@link java.nio.ByteBuffer} giving access to the bytes that hold the data being - * exported to the consumer, and positioned at a particular index. - *

- * Essentially this saves the client from computing the byte offset of a particular index. The - * client is free to navigate the underlying byte data through the ByteBuffer. - * - * @param index in the buffer to position the pointer - * @return a ByteBuffer onto the exported data contents, positioned at the indexed item. - */ - ByteBuffer getNIOByteBuffer(int index); - - /** - * Obtain a {@link java.nio.ByteBuffer} giving access to the bytes that hold the data being - * exported to the consumer, and positioned at a particular multi-dimensional index. - *

- * Essentially this saves the client from computing the byte offset of a particular index. The - * client is free to navigate the underlying byte data through the ByteBuffer. - * - * @param indices multidimensional index at which to position the pointer - * @return a ByteBuffer onto the exported data contents, positioned at the indexed item. - */ - ByteBuffer getNIOByteBuffer(int... indices); - - /** * Report whether the exporter is able to offer direct access to the exported storage as a Java * byte array (through the API that involves class {@link Pointer}), or only supports the * abstract API. See also {@link PyBUF#AS_ARRAY}. diff --git a/src/org/python/core/buffer/BaseArrayBuffer.java b/src/org/python/core/buffer/BaseArrayBuffer.java --- a/src/org/python/core/buffer/BaseArrayBuffer.java +++ b/src/org/python/core/buffer/BaseArrayBuffer.java @@ -57,7 +57,7 @@ } @Override - protected int byteIndex(int... indices) throws IndexOutOfBoundsException { + public int byteIndex(int... indices) throws IndexOutOfBoundsException { // BaseBuffer implementation can be simplified since if indices.length!=1 we error. checkDimension(indices.length); // throws if != 1 return byteIndex(indices[0]); 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 @@ -351,16 +351,13 @@ storeAtImpl(value, byteIndex(indices)); } - /** - * Convert an item index (for a one-dimensional buffer) to a checked absolute byte index in the - * actual storage being shared by the exporter. See {@link #byteIndex(int[])} for discussion. - * - * @param index item-index from consumer - * @return corresponding byte-index in actual storage - * @throws IndexOutOfBoundsException if the index <0 or ≥shape[0] + /* + * In this implementation, we throw IndexOutOfBoundsException if index < 0 or > shape[0], but we + * could rely on the array or ByteBuffer checks when indexing, especially the latter since + * position is checked against limit. */ - // XXX Consider making this part of the PyBuffer interface - protected int byteIndex(int index) throws IndexOutOfBoundsException { + @Override + public int byteIndex(int index) throws IndexOutOfBoundsException { // Treat as one-dimensional if (index < 0 || index >= shape[0]) { throw new IndexOutOfBoundsException(); @@ -368,25 +365,11 @@ return index0 + index * strides[0]; } - /** - * Convert a multi-dimensional item index (if we are not using indirection) to an absolute byte - * index in the actual storage array being shared by the exporter. The purpose of this method is - * to allow a sub-class to define, in one place, an indexing calculation that maps the index as - * provided by the consumer into an index in the storage known to the buffer. - *

- * In the usual case where the storage is referenced via a storage member and - * {@link #index0} member, the buffer implementation may use storage[byteIndex(i)] - * to reference the (first byte of) the item x[i]. This is what the default implementation of - * accessors in BaseArrayBuffer will do. In the simplest cases, calling - * byteIndex may be an overhead to avoid, and an implementation will specialise the - * accessors. The default implementation here is suited to N-dimensional arrays. - * - * @param indices n-dimensional item-index from consumer - * @return corresponding byte-index in actual storage - * @throws IndexOutOfBoundsException if any index <0 or ≥shape[i] + /* + * In this implementation, we throw IndexOutOfBoundsException if any index[i] < 0 or > shape[i]. */ - // XXX Consider making this part of the PyBuffer interface - protected int byteIndex(int... indices) throws IndexOutOfBoundsException { + @Override + public int byteIndex(int... indices) throws IndexOutOfBoundsException { final int N = checkDimension(indices); // In general: index0 + sum(k=0,N-1) indices[k]*strides[k] int index = index0; @@ -653,22 +636,6 @@ } @Override - public ByteBuffer getNIOByteBuffer(int index) { - // The buffer spans the whole storage but is positioned at index - ByteBuffer b = getNIOByteBufferImpl(); - b.position(byteIndex(index)); - return b; - } - - @Override - public ByteBuffer getNIOByteBuffer(int... indices) { - // The buffer spans the whole storage but is positioned at indices[] - ByteBuffer b = getNIOByteBufferImpl(); - b.position(byteIndex(indices)); - return b; - } - - @Override public boolean hasArray() { // AS_ARRAY is a non-navigation flag, so is inverted in gFeatureFlags return (gFeatureFlags & AS_ARRAY) == 0; // i.e. featureFlags & AS_ARRAY is true diff --git a/src/org/python/core/buffer/BaseNIOBuffer.java b/src/org/python/core/buffer/BaseNIOBuffer.java --- a/src/org/python/core/buffer/BaseNIOBuffer.java +++ b/src/org/python/core/buffer/BaseNIOBuffer.java @@ -78,7 +78,7 @@ } @Override - protected int byteIndex(int... indices) throws IndexOutOfBoundsException { + public int byteIndex(int... indices) throws IndexOutOfBoundsException { // BaseBuffer implementation can be simplified since if indices.length!=1 we error. checkDimension(indices.length); // throws if != 1 return byteIndex(indices[0]); @@ -123,7 +123,8 @@ if (count > 0) { - ByteBuffer src = getNIOByteBuffer(srcIndex); + ByteBuffer src = getNIOByteBuffer(); + int pos = byteIndex(srcIndex); // Pick up attributes necessary to choose an efficient copy strategy int itemsize = getItemsize(); @@ -132,12 +133,11 @@ // Strategy depends on whether items are laid end-to-end contiguously or there are gaps if (stride == itemsize) { // stride == itemsize: straight copy of contiguous bytes - src.limit(src.position() + count * itemsize); + src.limit(pos + count * itemsize).position(pos); dest.put(src); } else if (itemsize == 1) { // Non-contiguous copy: single byte items - int pos = src.position(); for (int i = 0; i < count; i++) { src.position(pos); dest.put(src.get()); @@ -146,7 +146,6 @@ } else { // Non-contiguous copy: each time, copy itemsize bytes then skip - int pos = src.position(); for (int i = 0; i < count; i++) { src.limit(pos + itemsize).position(pos); dest.put(src); @@ -184,7 +183,8 @@ if (count > 0) { - ByteBuffer dst = getNIOByteBuffer(dstIndex); + ByteBuffer dst = getNIOByteBuffer(); + int pos = byteIndex(dstIndex); // Pick up attributes necessary to choose an efficient copy strategy int itemsize = getItemsize(); @@ -194,11 +194,11 @@ // Strategy depends on whether items are laid end-to-end or there are gaps if (skip == 0) { // Straight copy of contiguous bytes + dst.position(pos); dst.put(src); } else if (itemsize == 1) { // Non-contiguous copy: single byte items - int pos = dst.position(); for (int i = 0; i < count; i++) { dst.position(pos); dst.put(src.get()); @@ -208,7 +208,6 @@ } else { // Non-contiguous copy: each time, copy itemsize bytes at a time - int pos = dst.position(); for (int i = 0; i < count; i++) { dst.position(pos); // Delineate the next itemsize bytes in the src 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 @@ -104,7 +104,7 @@ * and an item size of 1. */ @Override - protected int byteIndex(int index) throws IndexOutOfBoundsException { + public int byteIndex(int index) throws IndexOutOfBoundsException { if (index < 0 || index >= shape[0]) { throw new IndexOutOfBoundsException(); } diff --git a/src/org/python/core/buffer/SimpleNIOBuffer.java b/src/org/python/core/buffer/SimpleNIOBuffer.java --- a/src/org/python/core/buffer/SimpleNIOBuffer.java +++ b/src/org/python/core/buffer/SimpleNIOBuffer.java @@ -112,7 +112,7 @@ } @Override - protected int byteIndex(int index) throws IndexOutOfBoundsException { + public final int byteIndex(int index) throws IndexOutOfBoundsException { return index0 + index; } diff --git a/src/org/python/core/buffer/SimpleStringBuffer.java b/src/org/python/core/buffer/SimpleStringBuffer.java --- a/src/org/python/core/buffer/SimpleStringBuffer.java +++ b/src/org/python/core/buffer/SimpleStringBuffer.java @@ -68,7 +68,7 @@ * In SimpleStringBuffer we can simply return the argument. */ @Override - protected final int byteIndex(int index) { + public final int byteIndex(int index) { // We do not check the index because String will do it for us. return index; } @@ -97,8 +97,7 @@ public PyBuffer getBufferSlice(int flags, int start, int count) { if (count > 0) { // The new string content is just a sub-string. - return new SimpleStringView(getRoot(), flags, - bufString.substring(start, start + count)); + return new SimpleStringView(getRoot(), flags, bufString.substring(start, start + count)); } else { // Special case for count==0 where start out of bounds sometimes raises exception. return new ZeroByteBuffer.View(getRoot(), flags); 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 @@ -129,7 +129,7 @@ } @Override - protected final int byteIndex(int index) throws IndexOutOfBoundsException { + public final int byteIndex(int index) throws IndexOutOfBoundsException { if (index < 0 || index >= shape[0]) { throw new IndexOutOfBoundsException(); } diff --git a/src/org/python/core/buffer/Strided1DNIOBuffer.java b/src/org/python/core/buffer/Strided1DNIOBuffer.java --- a/src/org/python/core/buffer/Strided1DNIOBuffer.java +++ b/src/org/python/core/buffer/Strided1DNIOBuffer.java @@ -138,7 +138,7 @@ } @Override - protected int byteIndex(int index) throws IndexOutOfBoundsException { + public final int byteIndex(int index) throws IndexOutOfBoundsException { return index0 + index * stride; } diff --git a/src/org/python/core/buffer/ZeroByteBuffer.java b/src/org/python/core/buffer/ZeroByteBuffer.java --- a/src/org/python/core/buffer/ZeroByteBuffer.java +++ b/src/org/python/core/buffer/ZeroByteBuffer.java @@ -47,7 +47,7 @@ * In a ZeroByteBuffer, the index is always out of bounds. */ @Override - protected int byteIndex(int index) throws IndexOutOfBoundsException { + public int byteIndex(int index) throws IndexOutOfBoundsException { // This causes all access to the bytes in to throw (since BaseBuffer calls it). throw new IndexOutOfBoundsException(); } @@ -56,7 +56,7 @@ * In a ZeroByteBuffer, if the dimensions are right, the index is out of bounds anyway. */ @Override - protected int byteIndex(int... indices) throws IndexOutOfBoundsException { + public int byteIndex(int... indices) throws IndexOutOfBoundsException { // Bootless dimension check takes precedence (for consistency with other buffers) checkDimension(indices); // This causes all access to the bytes to throw (since BaseBuffer calls it). 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 @@ -1061,59 +1061,6 @@ } - /** Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer(int)}. */ - @Test - public void testGetNIOByteBufferInt() { - announce("getNIOByteBuffer (int)"); - - int n = ref.length, itemsize = view.getItemsize(); - byte[] exp = new byte[itemsize], bytes = ref.bytes; - - for (int i = 0; i < n; i++) { - // Expected result is one item (allow for itemsize) - int p = i * itemsize; - for (int j = 0; j < itemsize; j++) { - exp[j] = bytes[p + j]; - } - // Get buffer and check for correct data at bb.position() - ByteBuffer bb = view.getNIOByteBuffer(i); - ByteBufferTestSupport.assertBytesEqual("getNIOByteBuffer(int) value", exp, bb); - } - } - - /** Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer(int[])}. */ - @Test - public void testGetNIOByteBufferIntArray() { - int[] index = new int[1]; - announce("getNIOByteBuffer (n-dim)"); - - int n = ref.length, itemsize = view.getItemsize(); - byte[] exp = new byte[itemsize], bytes = ref.bytes; - - for (int i = 0; i < n; i++) { - // Expected result is one item (allow for itemsize) - int p = i * itemsize; - for (int j = 0; j < itemsize; j++) { - exp[j] = bytes[p + j]; - } - - // Get buffer and check for correct data at bb.position() - index[0] = i; - ByteBuffer bb = view.getNIOByteBuffer(index); - ByteBufferTestSupport.assertBytesEqual("getNIOByteBuffer(int[]) value", exp, bb); - } - - // Check 2D index throws - try { - view.getNIOByteBuffer(0, 0); - fail("Use of 2D index did not raise exception"); - } catch (PyException pye) { - // Expect BufferError - // XXX ... but should it be TypeError here? - assertEquals(Py.BufferError, pye.type); - } - } - /** Test method for {@link org.python.core.PyBuffer#hasArray()}. */ @Test public void testHasArray() { -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:24 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:24 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Add_a_test_of_PyBuffer=2Eco?= =?utf-8?q?pyFrom=28PyBuffer=29?= Message-ID: <20160827131224.18012.24802.7A7EF9FC@psf.io> https://hg.python.org/jython/rev/f02de625933c changeset: 7944:f02de625933c user: Jeff Allen date: Sat Jun 25 10:32:17 2016 +0100 summary: Add a test of PyBuffer.copyFrom(PyBuffer) Simply missing previously, but also a prelude to fixing the bug in the overlap-copy in memoryview. files: src/org/python/core/buffer/BaseBuffer.java | 1 + tests/java/org/python/core/PyBufferTest.java | 148 +++++++++- tests/java/org/python/core/PyBufferTestSupport.java | 8 +- 3 files changed, 145 insertions(+), 12 deletions(-) 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 @@ -531,6 +531,7 @@ @Override public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { + checkDimension(1); checkWritable(); int itemsize = getItemsize(); 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 @@ -21,6 +21,7 @@ import org.python.core.PyBufferTestSupport.ReadonlyExporterFactory; import org.python.core.PyBufferTestSupport.SlicedTestSpec; import org.python.core.PyBufferTestSupport.TestSpec; +import org.python.core.PyBufferTestSupport.TestSpec.ObjectAndView; import org.python.core.PyBufferTestSupport.WritableExporterFactory; import org.python.core.buffer.BaseBuffer; import org.python.core.buffer.SimpleBuffer; @@ -130,14 +131,7 @@ // Tests using local types of exporter - ExporterFactory simpleExporter = new ReadonlyExporterFactory() { - - @Override - public BufferProtocol make(ByteMaterial m) { - return new SimpleExporter(m.getBytes()); - } - - }; + ExporterFactory simpleExporter = new SimpleExporterFactory(); s.add(simpleExporter, byteMaterial); ExporterFactory simpleWritableExporter = new WritableExporterFactory() { @@ -562,6 +556,134 @@ } } + /** Test method for {@link org.python.core.PyBuffer#copyFrom(byte[], int, int, int)}. */ + @Test + public void testCopyFromPyBuffer() { + announce("copyFrom"); + + /* + * The test material (this time) presents a view that has n items of size i, that are spaced + * in the underlying buffer with stride s. + */ + int n = spec.ref.length; + int s = spec.getStride(); + int i = spec.getItemsize(); // = 1 in all test cases at time of writing + + // The material we copy to it should have these strides: + int[] srcStrides; + if (n < 2) { + srcStrides = new int[] {i}; + } else if (s > 2 * i || s < -2 * i) { + srcStrides = new int[] {i, s - i, s, s + i, -s + i, -s, -s - i}; + } else if (s == 2 * i || s == -2 * i) { + srcStrides = new int[] {i, 2 * i, 3 * i, -i, -2 * i, -3 * i}; + } else { // ( s==i || s==-i ) + srcStrides = new int[] {i, 2 * i, -i, -2 * i}; + } + + // Also need the maximum absolute value + int maxStride = 0; + for (int t : srcStrides) { + if (t > maxStride) { + maxStride = t; + } else if (-t > maxStride) { + maxStride = -t; + } + } + + // And these offsets to the lowest-indexed source item + int maxOffset = n + 1; + int[] srcOffsets = new int[] {0, (maxOffset + 1) / 3, maxOffset}; + + // Make the source material to copy from, big enough to accommodate n strides + int srcMaterialSize = n * maxStride + maxOffset; + ByteMaterial srcMaterial = new ByteMaterial(48, srcMaterialSize, i); + + /* + * Now we need a series of PyBuffer views on the source data, sliced and offset according to + * the offset and stride values we have enumerated. We'd like to use the same factory as the + * current test view (this.view), because copy from its own type might be optimised, and a + * different, bog-standard factory to test the general case. + */ + ExporterFactory[] factories = {spec.factory, new SimpleExporterFactory()}; + + for (ExporterFactory factory : factories) { + + /* + * We'll use the same apparatus to create the source buffer as we use to make the test + * cases. The specifications for them will all be derived from this one: + */ + TestSpec original = new TestSpec(factory, srcMaterial); + + /* + * Do this where the pattern of indices constituting src overlaps (or not) the pattern + * of view in challenging ways, including greater and smaller strides. + */ + + for (int stride : srcStrides) { + for (int offset : srcOffsets) { + int start = (stride > 0) ? offset : srcMaterialSize - offset - i; + doTestCopyFrom(original, start, n, stride); + } + } + } + + } + + // XXX Separately test where src is a view on is the same object. + + + + /** Helper function for {@link #testCopyFromPyBuffer()} + */ + private void doTestCopyFrom(TestSpec original, int start, int n, int stride) { + + // Derive sliced test material from the original + TestSpec srcSpec = new SlicedTestSpec(original, 1, start, n, stride); + ObjectAndView pair = srcSpec.makePair(); + PyBuffer src = pair.view; + byte[] srcBytes = srcSpec.ref.bytes; + + // And for the test object + int s = spec.getStart(); + int p = spec.getStride(); + String srcName = pair.obj.getClass().getSimpleName(); + if (verbosity > 1) { + System.out.printf(" copy src[%d:%d:%d] %s(%d) to obj[%d:%d:%d]\n", + start, start+n*stride, stride, srcName, + n, s, s + n * p, p); + } + + // Initialise the destination object and view (have to do each time) from spec + createObjAndView(); + + // Our test is against the underlying object of which the view may be a slice + TestSpec underlying = spec.getOriginal(); + PyBuffer underlyingView = obj.getBuffer(underlying.flags & ~PyBUF.WRITABLE); + byte[] before = bytesFromByteAt(underlyingView); + + if (!spec.readonly) { + // This is the call we are testing (a write operation). + view.copyFrom(src); + + // Our test is against the underlying object of which the view may be a slice + byte[] after = bytesFromByteAt(underlyingView); + + // Test that the corresponding bytes of the underlying object match data copied in + ByteBufferTestSupport.checkWriteCorrect(before, after, srcBytes, 0, n, 1, s, p); + + } else { + // Should fail (write operation) + try { + view.copyFrom(src); + fail("Write access not prevented: " + spec); + } catch (PyException pye) { + // Expect TypeError only if the buffer was readonly + assertEquals(Py.TypeError, pye.type); + } + } + } + /** * Test method for {@link org.python.core.BufferProtocol#getBuffer()} and * {@link org.python.core.PyBuffer#getBuffer()}. @@ -1116,6 +1238,16 @@ } + /** A factory for SimpleBuffer objects used in genTestSpects and some tests. */ + private static class SimpleExporterFactory extends ReadonlyExporterFactory { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new SimpleExporter(m.getBytes()); + } + + } + /** * Base class of certain exporters that permit testing abstracted from the Jython interpreter. */ diff --git a/tests/java/org/python/core/PyBufferTestSupport.java b/tests/java/org/python/core/PyBufferTestSupport.java --- a/tests/java/org/python/core/PyBufferTestSupport.java +++ b/tests/java/org/python/core/PyBufferTestSupport.java @@ -467,19 +467,19 @@ * specification. * * @param parent specification of byte buffer to slice - * @param size number of consecutive bytes forming one item + * @param itemsize number of consecutive bytes forming one item * @param first byte-index in the parent of byte 0 of item 0 the result * @param count number of items in the slice * @param step byte-index increment in the parent between items */ - SlicedTestSpec(TestSpec parent, int size, int first, int count, int step) { - super(parent, parent.ref.slice(size, first, count, step), new int[] {count}, + SlicedTestSpec(TestSpec parent, int itemsize, int first, int count, int step) { + super(parent, parent.ref.slice(itemsize, first, count, step), new int[] {count}, new int[1], strided1DFlags, strided1DTassles); // It only seems to make sense for byte-array parent (or does all scale?) if (parent.getItemsize() != 1) { throw new IllegalArgumentException("Only byte-array parent supported"); } - this.itemsize = size; + this.itemsize = itemsize; // Write these down verbatim for subsequent call to getBufferSlice this.first = first; this.count = count; -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:24 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:24 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Refactor_to_introduce_Base1?= =?utf-8?q?DBuffer=2E?= Message-ID: <20160827131224.18218.73282.89AB4D53@psf.io> https://hg.python.org/jython/rev/a3abfca10951 changeset: 7943:a3abfca10951 user: Jeff Allen date: Thu Jun 23 12:52:53 2016 +0100 summary: Refactor to introduce Base1DBuffer. Reduces duplication between Array and NIO. BaseBuffer is now distinctly the home of generic implementations and Base1DBuffer is the home of all one-dimensional specialisation. Constructors low in the stack now take navigation arrays as arguments, favouring clarity of purpose over minor (illusory?) speed gains. files: src/org/python/core/buffer/Base1DBuffer.java | 117 ++++++++++ src/org/python/core/buffer/BaseArrayBuffer.java | 89 +------ src/org/python/core/buffer/BaseBuffer.java | 75 +++--- src/org/python/core/buffer/BaseNIOBuffer.java | 89 +------ src/org/python/core/buffer/SimpleBuffer.java | 38 +-- src/org/python/core/buffer/SimpleNIOBuffer.java | 19 +- src/org/python/core/buffer/SimpleStringBuffer.java | 3 +- src/org/python/core/buffer/Strided1DBuffer.java | 16 +- src/org/python/core/buffer/Strided1DNIOBuffer.java | 64 ++--- src/org/python/core/buffer/Strided1DWritableBuffer.java | 10 +- src/org/python/core/buffer/ZeroByteBuffer.java | 8 +- tests/java/org/python/core/PyBufferNIOTest.java | 8 +- tests/java/org/python/core/PyBufferTest.java | 6 +- 13 files changed, 249 insertions(+), 293 deletions(-) diff --git a/src/org/python/core/buffer/Base1DBuffer.java b/src/org/python/core/buffer/Base1DBuffer.java new file mode 100644 --- /dev/null +++ b/src/org/python/core/buffer/Base1DBuffer.java @@ -0,0 +1,117 @@ +package org.python.core.buffer; + +import org.python.core.PyBUF; + +/** + * Base implementation of the Buffer API appropriate to 1-dimensional arrays, of any item size, + * independent of the storage implementation. The description of {@link BaseBuffer} mostly applies. + */ +public abstract class Base1DBuffer extends BaseBuffer { + + /** The strides array for a contiguous 1D byte buffer. */ + protected static final int[] ONE = {1}; + + /** The shape array for a zero length 1D buffer. */ + protected static final int[] ZERO = {0}; + + /** + * Construct an instance of Base1DBuffer in support of a sub-class, specifying the + * 'feature flags', or at least a starting set to be adjusted later. Also specify the navigation + * ( {@link #index0}, number of elements, and {@link #strides} array. These 'feature flags' are + * the features of the buffer exported, not the flags that form the consumer's request. The + * buffer will be read-only unless {@link PyBUF#WRITABLE} is set. {@link PyBUF#FORMAT} is + * implicitly added to the feature flags. + *

+ * To complete initialisation, the sub-class normally must create its own wrapped byte-storage, + * and call {@link #checkRequestFlags(int)} passing the consumer's request flags. + * + * @param featureFlags bit pattern that specifies the features allowed + * @param index0 index into storage of item[0] + * @param size number of elements in the view + * @param strides an array of length 1 providing index stride between successive elements + */ + protected Base1DBuffer(int featureFlags, int index0, int size, int[] strides) { + super(featureFlags, index0, size == 0 ? ZERO : new int[] {size}, strides); + } + + /** + * Construct an instance of Base1DBuffer in support of a sub-class, specifying the + * 'feature flags', or at least a starting set to be adjusted later. Also specify the navigation + * ( {@link #index0}, number of elements, and byte-index distance from one to the next. These + * 'feature flags' are the features of the buffer exported, not the flags that form the + * consumer's request. The buffer will be read-only unless {@link PyBUF#WRITABLE} is set. + * {@link PyBUF#FORMAT} is implicitly added to the feature flags. + *

+ * To complete initialisation, the sub-class normally must create its own wrapped byte-storage, + * and call {@link #checkRequestFlags(int)} passing the consumer's request flags. + * + * @param featureFlags bit pattern that specifies the features allowed + * @param index0 index into storage of item[0] + * @param size number of elements in the view + * @param stride byte-index distance from one element to the next + */ + protected Base1DBuffer(int featureFlags, int index0, int size, int stride) { + this(featureFlags, index0, size, stride == 1 ? ONE : new int[] {stride}); + } + + @Override + protected int getSize() { + return shape[0]; + } + + @Override + public int getLen() { + return shape[0] * getItemsize(); + } + + /** + * {@inheritDoc} + *

+ * Specialised to one-dimensional, possibly strided buffer. + */ + @Override + protected int calcGreatestIndex() { + int stride = strides[0]; + if (stride == 1) { + return index0 + shape[0] - 1; + } else if (stride > 0) { + return index0 + (shape[0] - 1) * stride; + } else { + return index0 - 1; + } + } + + /** + * {@inheritDoc} + *

+ * Specialised to one-dimensional, possibly strided buffer. + */ + @Override + protected int calcLeastIndex() { + int stride = strides[0]; + if (stride < 0) { + return index0 + (shape[0] - 1) * stride; + } else { + return index0; + } + } + + /** + * {@inheritDoc} + *

+ * Specialised in BaseArrayBuffer to one dimension. + */ + @Override + public boolean isContiguous(char order) { + if ("CFA".indexOf(order) < 0) { + return false; + } else { + if (getShape()[0] < 2) { + return true; + } else { + return getStrides()[0] == getItemsize(); + } + } + } + +} diff --git a/src/org/python/core/buffer/BaseArrayBuffer.java b/src/org/python/core/buffer/BaseArrayBuffer.java --- a/src/org/python/core/buffer/BaseArrayBuffer.java +++ b/src/org/python/core/buffer/BaseArrayBuffer.java @@ -11,7 +11,7 @@ * The description of {@link BaseBuffer} mostly applies. Methods provided or overridden here are * appropriate to 1-dimensional arrays, of any item size, backed by byte[]. */ -public abstract class BaseArrayBuffer extends BaseBuffer implements PyBuffer { +public abstract class BaseArrayBuffer extends Base1DBuffer { /** * Reference to the underlying byte[] storage that the exporter is sharing with the @@ -24,29 +24,24 @@ /** * Construct an instance of BaseArrayBuffer in support of a sub-class, specifying - * the 'feature flags', or at least a starting set to be adjusted later. These are the features - * of the buffer exported, not the flags that form the consumer's request. The buffer will be - * read-only unless {@link PyBUF#WRITABLE} is set in the feature flags. {@link PyBUF#FORMAT} and - * {@link PyBUF#AS_ARRAY} are implicitly added to the feature flags. The navigation arrays are - * all null, awaiting action by the sub-class constructor. To complete initialisation, the - * sub-class normally must assign: the buffer ( {@link #storage}, {@link #index0}), and the - * navigation arrays ({@link #shape}, {@link #strides}), and call - * {@link #checkRequestFlags(int)} passing the consumer's request flags. + * the 'feature flags', or at least a starting set to be adjusted later. Also specify the + * navigation ( {@link #index0}, number of elements, and stride. These 'feature flags' are the + * features of the buffer exported, not the flags that form the consumer's request. The buffer + * will be read-only unless {@link PyBUF#WRITABLE} is set. {@link PyBUF#FORMAT} and + * {@link PyBUF#AS_ARRAY} are implicitly added to the feature flags. + *

+ * To complete initialisation, the sub-class normally must call {@link #checkRequestFlags(int)} + * passing the consumer's request flags. * - * @param featureFlags bit pattern that specifies the actual features allowed/required + * @param storage the array of bytes storing the implementation of the exporting object + * @param featureFlags bit pattern that specifies the features allowed + * @param index0 index into storage of item[0] + * @param size number of elements in the view + * @param stride byte-index distance from one element to the next */ - protected BaseArrayBuffer(int featureFlags) { - super(featureFlags | AS_ARRAY); - } - - @Override - protected int getSize() { - return shape[0]; - } - - @Override - public int getLen() { - return shape[0] * getItemsize(); + protected BaseArrayBuffer(byte[] storage, int featureFlags, int index0, int size, int stride) { + super(featureFlags | AS_ARRAY, index0, size, stride); + this.storage = storage; } @Override @@ -71,38 +66,6 @@ /** * {@inheritDoc} *

- * Specialised to one-dimensional, possibly strided buffer. - */ - @Override - protected int calcGreatestIndex() { - int stride = strides[0]; - if (stride == 1) { - return index0 + shape[0] - 1; - } else if (stride > 0) { - return index0 + (shape[0] - 1) * stride; - } else { - return index0 - 1; - } - } - - /** - * {@inheritDoc} - *

- * Specialised to one-dimensional, possibly strided buffer. - */ - @Override - protected int calcLeastIndex() { - int stride = strides[0]; - if (stride < 0) { - return index0 + (shape[0] - 1) * stride; - } else { - return index0; - } - } - - /** - * {@inheritDoc} - *

* The implementation in BaseArrayBuffer deals with the general one-dimensional * case of arbitrary item size and stride. */ @@ -256,22 +219,4 @@ public Pointer getBuf() { return new Pointer(storage, index0); } - - /** - * {@inheritDoc} - *

- * Specialised in BaseArrayBuffer to one dimension. - */ - @Override - public boolean isContiguous(char order) { - if ("CFA".indexOf(order) < 0) { - return false; - } else { - if (getShape()[0] < 2) { - return true; - } else { - return getStrides()[0] == getItemsize(); - } - } - } } diff --git a/src/org/python/core/buffer/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 @@ -16,22 +16,22 @@ * unglamorous common code need only be implemented once. *

* This class leaves undefined the storage mechanism for the bytes (typically byte[] or - * java.nio.ByteBuffer). A concrete class that extends this one must provide elementary - * accessors {@link #byteAtImpl(int)}, {@link #storeAtImpl(byte, int)} that abstract this storage, a - * factory {@link #getNIOByteBufferImpl()} for ByteBuffers that wrap the storage, and a - * factory for slices {@link #getBufferSlice(int, int, int, int)}. The constructor must specify the - * feature flags (see {@link #BaseBuffer(int)}), set {@link #index0}, {@link #shape} and - * {@link #strides}, and finally check the client capabilities with {@link #checkRequestFlags(int)}. - * Sub-classes intended to represent slices of exporters that must count their exports as part of a - * locking protocol, as does bytearray, must override {@link #getRoot()} so that a - * buffer view {@link #release()} on a slice, propagates to the buffer view that provided it. + * java.nio.ByteBuffer), while remaining definite that it is an indexable sequence of + * bytes. A concrete class that extends this one must provide elementary accessors + * {@link #byteAtImpl(int)}, {@link #storeAtImpl(byte, int)} that abstract this storage, a factory + * {@link #getNIOByteBufferImpl()} for ByteBuffers that wrap the storage, and a factory + * for slices {@link #getBufferSlice(int, int, int, int)}. + *

+ * The sub-class constructor must specify the feature flags (see {@link #BaseBuffer(int)}), set + * {@link #index0}, {@link #shape} and {@link #strides}, and finally check the client capabilities + * with {@link #checkRequestFlags(int)}. Sub-classes intended to represent slices of exporters that + * must count their exports as part of a locking protocol, as does bytearray, must + * override {@link #getRoot()} so that a buffer view {@link #release()} on a slice, propagates to + * the buffer view that provided it. *

* Access methods provided here necessarily work with the abstracted {@link #byteAtImpl(int)}, * {@link #storeAtImpl(byte, int)} interface, but subclasses are able to override them with more - * efficient versions that employ knowledge of the particular storage type used. The provided buffer - * access methods may be restricted to 1-dimensional arrays where the units are single bytes, stored - * contiguously. Sub-classes that deal with N-dimensional arrays, non-contiguous storage and items - * that are not single bytes must sometimes override the default implementations. + * efficient versions that employ knowledge of the particular storage type used. *

* This base implementation is writable only if {@link PyBUF#WRITABLE} is in the feature flags * passed to the constructor. Otherwise, all methods for write access raise a TypeError @@ -60,11 +60,6 @@ protected int[] strides; /** - * The strides array for a contiguous byte buffer.. - */ - protected static final int[] CONTIG_STRIDES = {1}; - - /** * Absolute byte-index in the storage of item[0]. In one dimension, for a positive * stride this is equal to the offset of the first byte used in whatever * byte-storage is provided, and for a negative stride it is the first byte of the @@ -124,19 +119,25 @@ /** * Construct an instance of BaseBuffer in support of a sub-class, specifying the - * 'feature flags', or at least a starting set to be adjusted later. These are the features of - * the buffer exported, not the flags that form the consumer's request. The buffer will be - * read-only unless {@link PyBUF#WRITABLE} is set in the feature flags. {@link PyBUF#FORMAT} is - * implicitly added to the feature flags. The navigation arrays are all null, awaiting action by - * the sub-class constructor. To complete initialisation, the sub-class normally must create its - * own wrapped byte-storage, assign {@link #index0}) and the navigation arrays ( {@link #shape}, - * {@link #strides}), and call {@link #checkRequestFlags(int)} passing the consumer's request - * flags. + * 'feature flags', or at least a starting set to be adjusted later. Also specify the navigation + * ( {@link #index0}, {@link #shape}, and {@link #strides}). These 'feature flags' are the + * features of the buffer exported, not the flags that form the consumer's request. The buffer + * will be read-only unless {@link PyBUF#WRITABLE} is set. {@link PyBUF#FORMAT} is implicitly + * added to the feature flags. + *

+ * To complete initialisation, the sub-class normally must create its own wrapped byte-storage, + * and call {@link #checkRequestFlags(int)} passing the consumer's request flags. * - * @param featureFlags bit pattern that specifies the actual features allowed/required + * @param featureFlags bit pattern that specifies the features allowed + * @param index0 index into storage of item[0,...,0] + * @param shape elements in each dimension + * @param strides between successive elements in each dimension */ - protected BaseBuffer(int featureFlags) { + protected BaseBuffer(int featureFlags, int index0, int[] shape, int[] strides) { setFeatureFlags(featureFlags | FORMAT); + this.index0 = index0; + this.shape = shape; + this.strides = strides; } /** @@ -636,7 +637,7 @@ // The buffer spans the whole storage ByteBuffer b = getNIOByteBufferImpl(); // For the one-dimensional contiguous case it makes sense to set the limit: - if (shape.length == 1) { + if (shape.length == 1 && isContiguous('A')) { int stride = strides[0]; if (getItemsize() == stride) { b.limit(index0 + shape[0] * stride); @@ -707,8 +708,8 @@ /* * If we were to compute the strides array for a C-contiguous array, the last stride would * equal the item size, and generally stride[k-1] = shape[k]*stride[k]. This is the basis of - * the test. However, note that for any k where shape[k]==1 there is no "next sub-array" - * and no discontiguity. + * the test. However, note that for any k where shape[k]==1 there is no "next sub-array" and + * no discontiguity. */ final int N = shape.length; /* @@ -731,16 +732,16 @@ private boolean isFortranContiguous() { /* - * If we were to compute the strides array for a Fortran-contiguous array, the first stride would - * equal the item size, and generally stride[k+1] = shape[k]*stride[k]. This is the basis of - * the test. However, note that for any k where shape[k]==1 there is no "next sub-array" - * and no discontiguity. + * If we were to compute the strides array for a Fortran-contiguous array, the first stride + * would equal the item size, and generally stride[k+1] = shape[k]*stride[k]. This is the + * basis of the test. However, note that for any k where shape[k]==1 there is no + * "next sub-array" and no discontiguity. */ final int N = shape.length; /* * size is the stride in bytes-index from item[0,...,0,ik,0,...,0] to - * item[0,...,0,ik+1,0,...,0]. Start the iteration at k=0. An increment of one - * in the first index makes a stride of the item size. + * item[0,...,0,ik+1,0,...,0]. Start the iteration at k=0. An increment of one in the first + * index makes a stride of the item size. */ int size = getItemsize(); for (int k = 0; k < N; k++) { diff --git a/src/org/python/core/buffer/BaseNIOBuffer.java b/src/org/python/core/buffer/BaseNIOBuffer.java --- a/src/org/python/core/buffer/BaseNIOBuffer.java +++ b/src/org/python/core/buffer/BaseNIOBuffer.java @@ -14,7 +14,7 @@ * provided or overridden here are appropriate to 1-dimensional arrays, of any item size, backed by * a ByteBuffer. */ -public abstract class BaseNIOBuffer extends BaseBuffer implements PyBuffer { +public abstract class BaseNIOBuffer extends Base1DBuffer { /** * A {@link java.nio.ByteBuffer} (possibly a direct buffer) wrapping the storage that the @@ -32,22 +32,27 @@ protected ByteBuffer storage; /** - * Partially construct an instance of BaseNIOBuffer in support of a sub-class, - * specifying the 'feature flags', or at least a starting set to be adjusted later. These are - * the features of the buffer exported, not the flags that form the consumer's request. The - * buffer will be read-only and/or backed by a (heap) array according to the properties of the - * ByteBuffer passed in. {@link PyBUF#FORMAT} is implicitly added to the feature - * flags. To complete initialisation, the sub-class normally must assign: {@link #index0}) and - * the navigation arrays ({@link #shape}, {@link #strides}), and call - * {@link #checkRequestFlags(int)} passing the consumer's request flags. + * Construct an instance of BaseNIOBuffer in support of a sub-class, specifying the + * 'feature flags', or at least a starting set to be adjusted later. Also specify the navigation + * ( {@link #index0}, number of elements, and stride. These 'feature flags' are the features of + * the buffer exported, not the flags that form the consumer's request. The buffer will be + * read-only unless {@link PyBUF#WRITABLE} is set. {@link PyBUF#FORMAT} and + * {@link PyBUF#AS_ARRAY} are implicitly added to the feature flags. + *

+ * To complete initialisation, the sub-class normally must call {@link #checkRequestFlags(int)} + * passing the consumer's request flags. * * @param storage the ByteBuffer wrapping the exported object state. NOTE: this * PyBuffer keeps a reference and may manipulate the position, mark and * limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy. - * @param featureFlags bit pattern that specifies the actual features allowed/required + * @param featureFlags bit pattern that specifies the features allowed + * @param index0 index into storage of item[0] + * @param size number of elements in the view + * @param stride byte-index step between successive elements */ - protected BaseNIOBuffer(ByteBuffer storage, int featureFlags) { - super(featureFlags & ~(WRITABLE | AS_ARRAY)); + + protected BaseNIOBuffer(ByteBuffer storage, int featureFlags, int index0, int size, int stride) { + super(featureFlags & ~(WRITABLE | AS_ARRAY), index0, size, stride); this.storage = storage; // Deduce other feature flags from the client's ByteBuffer @@ -60,16 +65,6 @@ } @Override - protected int getSize() { - return shape[0]; - } - - @Override - public int getLen() { - return shape[0] * getItemsize(); - } - - @Override protected byte byteAtImpl(int byteIndex) throws IndexOutOfBoundsException { return storage.get(byteIndex); } @@ -92,38 +87,6 @@ /** * {@inheritDoc} *

- * Specialised to one-dimensional, possibly strided buffer. - */ - @Override - protected int calcGreatestIndex() { - int stride = strides[0]; - if (stride == 1) { - return index0 + shape[0] - 1; - } else if (stride > 0) { - return index0 + (shape[0] - 1) * stride; - } else { - return index0 - 1; - } - } - - /** - * {@inheritDoc} - *

- * Specialised to one-dimensional, possibly strided buffer. - */ - @Override - protected int calcLeastIndex() { - int stride = strides[0]; - if (stride < 0) { - return index0 + (shape[0] - 1) * stride; - } else { - return index0; - } - } - - /** - * {@inheritDoc} - *

* The default implementation in BaseNIOBuffer deals with the general * one-dimensional case of arbitrary item size and stride. */ @@ -324,22 +287,4 @@ checkHasArray(); return new Pointer(storage.array(), index0); } - - /** - * {@inheritDoc} - *

- * Specialised in BaseArrayBuffer to one dimension. - */ - @Override - public boolean isContiguous(char order) { - if ("CFA".indexOf(order) < 0) { - return false; - } else { - if (getShape()[0] < 2) { - return true; - } else { - return getStrides()[0] == getItemsize(); - } - } - } } diff --git a/src/org/python/core/buffer/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 @@ -9,27 +9,14 @@ */ public class SimpleBuffer extends BaseArrayBuffer { - /** - * Provide an instance of SimpleBuffer with navigation variables partly - * initialised, for sub-class use. One-dimensional arrays without strides are C- and - * F-contiguous. To complete initialisation, the sub-class must normally assign the buffer ( - * {@link #storage}, {@link #index0}), and the navigation ({@link #shape} array), and then call - * {@link #checkRequestFlags(int)} passing the consumer's request flags. - */ - protected SimpleBuffer() { - super(CONTIGUITY | SIMPLE); - // Initialise navigation - shape = new int[1]; - strides = CONTIG_STRIDES; - // suboffsets is always null for this type. - } /** * Provide an instance of SimpleBuffer with navigation variables initialised, for * sub-class use. The buffer ({@link #storage}, {@link #index0}), and the {@link #shape} array - * will be initialised from the arguments (which are checked for range). The {@link #strides} is - * set for (one-byte) unit stride. Only the call to {@link #checkRequestFlags(int)}, passing the - * consumer's request flags really remains for the sub-class constructor to do. + * will be initialised from the arguments (which are not checked for range). The + * {@link #strides} is set for a one-byte stride. Only the call to + * {@link #checkRequestFlags(int)}, passing the consumer's request flags really remains for the + * sub-class constructor to do. * *

      * super(storage, index0, size);
@@ -40,21 +27,10 @@
      * @param index0 offset where the data starts in that array (item[0])
      * @param size the number of bytes occupied
      * @throws NullPointerException if storage is null
-     * @throws ArrayIndexOutOfBoundsException if index0 and size are
-     *             inconsistent with storage.length
      */
     protected SimpleBuffer(byte[] storage, int index0, int size) throws PyException,
             ArrayIndexOutOfBoundsException {
-        this();
-        this.storage = storage;         // Exported data
-        // Initialise navigation
-        this.index0 = index0;           // Index to be treated as item[0]
-        shape[0] = size;                // Number of items in exported data
-
-        // Check arguments using the "all non-negative" trick
-        if ((index0 | size | storage.length - (index0 + size)) < 0) {
-            throw new ArrayIndexOutOfBoundsException();
-        }
+        super(storage, CONTIGUITY | SIMPLE, index0, size, 1);
     }
 
     /**
@@ -75,6 +51,10 @@
             ArrayIndexOutOfBoundsException, NullPointerException {
         this(storage, index0, size);    // Construct checked SimpleBuffer
         checkRequestFlags(flags);       // Check request is compatible with type
+        // Check arguments using the "all non-negative" trick
+        if ((index0 | size | storage.length - (index0 + size)) < 0) {
+            throw new ArrayIndexOutOfBoundsException();
+        }
     }
 
     /**
diff --git a/src/org/python/core/buffer/SimpleNIOBuffer.java b/src/org/python/core/buffer/SimpleNIOBuffer.java
--- a/src/org/python/core/buffer/SimpleNIOBuffer.java
+++ b/src/org/python/core/buffer/SimpleNIOBuffer.java
@@ -6,7 +6,7 @@
 import org.python.core.PyException;
 
 /**
- * Buffer API over a read-only one-dimensional array of one-byte items.
+ * Buffer API over a read-only one-dimensional java.nio.ByteBuffer of one-byte items.
  */
 
 public class SimpleNIOBuffer extends BaseNIOBuffer {
@@ -40,16 +40,7 @@
      */
     protected SimpleNIOBuffer(ByteBuffer storage, int index0, int size) throws PyException,
             ArrayIndexOutOfBoundsException {
-        super(storage, CONTIGUITY | SIMPLE);
-
-        // Initialise navigation
-        shape = new int[] {size};       // Number of items in exported data
-        strides = SIMPLE_STRIDES;
-        // suboffsets is always null for this type.
-
-        this.storage = storage;         // Exported data
-        this.index0 = index0;           // Index to be treated as item[0]
-
+        super(storage, CONTIGUITY | SIMPLE, index0, size, 1);
         // Check arguments using the "all non-negative" trick
         if ((index0 | size | storage.capacity() - (index0 + size)) < 0) {
             throw new ArrayIndexOutOfBoundsException();
@@ -181,11 +172,11 @@
          * @param flags the request flags of the consumer that requested the slice
          * @param storage ByteBuffer wrapping exported data (no reference kept)
          * @param offset where the data starts in that buffer (item[0])
-         * @param size the number of bytes occupied
+         * @param count the number of items in the sliced view
          */
-        public SimpleView(PyBuffer root, int flags, ByteBuffer storage, int offset, int size) {
+        public SimpleView(PyBuffer root, int flags, ByteBuffer storage, int offset, int count) {
             // Create a new SimpleNIOBuffer on the buffer passed in (part of the root)
-            super(flags, storage, offset, size);
+            super(flags, storage, offset, count);
             // Get a lease on the root PyBuffer
             this.root = root.getBuffer(FULL_RO);
         }
diff --git a/src/org/python/core/buffer/SimpleStringBuffer.java b/src/org/python/core/buffer/SimpleStringBuffer.java
--- a/src/org/python/core/buffer/SimpleStringBuffer.java
+++ b/src/org/python/core/buffer/SimpleStringBuffer.java
@@ -34,10 +34,9 @@
          * Leaving storage=null is ok because we carefully override every method that uses it,
          * deferring creation of the storage byte array until we absolutely must have one.
          */
-        super();
+        super(null, 0, bufString.length());
         // Save the backing string
         this.bufString = bufString;
-        shape[0] = bufString.length();
         // Check request is compatible with type
         checkRequestFlags(flags);
     }
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
@@ -58,12 +58,8 @@
      */
     protected Strided1DBuffer(byte[] storage, int index0, int count, int stride)
             throws ArrayIndexOutOfBoundsException, NullPointerException {
-        super(STRIDES);
-        this.storage = storage;         // Exported data
-        this.index0 = index0;           // Index to be treated as item[0]
-        this.shape = new int[] {count}; // Number of items in exported data
+        super(storage, STRIDES, index0, count, stride);
         this.stride = stride;           // Between items
-        this.strides = new int[] {stride};
 
         if (count == 0) {
             // Nothing to check as we'll make no accesses
@@ -114,7 +110,7 @@
      * @param storage raw byte array containing exported data
      * @param index0 index into storage of item[0]
      * @param count number of items in the slice
-     * @param stride in between successive elements of the new PyBuffer
+     * @param stride byte-index distance from one element to the next in the new PyBuffer
      * @throws NullPointerException if storage is null
      * @throws ArrayIndexOutOfBoundsException if index0, count and
      *             stride are inconsistent with storage.length
@@ -196,14 +192,14 @@
          * @param flags consumer requirements
          * @param storage raw byte array containing exported data
          * @param index0 index into storage of item[0]
-         * @param len number of items in the slice
+         * @param count number of items in the sliced view
          * @param stride in between successive elements of the new PyBuffer
          * @throws PyException (BufferError) when expectations do not correspond with the type
          */
-        public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int len, int stride)
-                throws PyException {
+        public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int count,
+                int stride) throws PyException {
             // Create a new on the buffer passed in (part of the root)
-            super(flags, storage, index0, len, stride);
+            super(flags, storage, index0, count, stride);
             // Get a lease on the root PyBuffer (read-only)
             this.root = root.getBuffer(FULL_RO);
         }
diff --git a/src/org/python/core/buffer/Strided1DNIOBuffer.java b/src/org/python/core/buffer/Strided1DNIOBuffer.java
--- a/src/org/python/core/buffer/Strided1DNIOBuffer.java
+++ b/src/org/python/core/buffer/Strided1DNIOBuffer.java
@@ -5,18 +5,17 @@
 import org.python.core.PyBuffer;
 import org.python.core.PyException;
 
-
 /**
- * Read-only buffer API over a one-dimensional array of one-byte items, that are evenly-spaced in a
- * storage array. The buffer has storage, index0 and length
- * properties in the usual way, designating a slice (or all) of a byte array, but also a
- * stride property (equal to getStrides()[0]).
+ * Read-only buffer API over a one-dimensional java.nio.ByteBuffer of one-byte items,
+ * that are evenly-spaced in that store. The buffer has storage, index0
+ * and length properties in the usual way, designating a slice (or all) of a byte
+ * array, but also a stride property (equal to getStrides()[0]).
  * 

- * Let the underlying buffer be the byte array u(i) for i=0..N-1, let x be the - * Strided1DNIOBuffer, and let the stride be p. The storage works as follows. - * Designate by x(j), for j=0..L-1, the byte at index j, that is, the byte - * retrieved by x.byteAt(j). Thus, we store x(j) at u(a+pj), that is, - * x(0) = u(a). When we construct such a buffer, we have to supply a = + * Let the underlying buffer be the byte sequence u(i) for i=0..N-1, let x be + * the Strided1DNIOBuffer, and let the stride be p. The storage works as + * follows. Designate by x(j), for j=0..L-1, the byte at index j, that is, the + * byte retrieved by x.byteAt(j). Thus, we store x(j) at u(a+pj), that + * is, x(0) = u(a). When we construct such a buffer, we have to supply a = * index0, L = count, and p = stride as the * constructor arguments. The last item in the slice x(L-1) is stored at u(a+p(L-1)). * For the simple case of positive stride, constructor argument index0 is the low index @@ -40,7 +39,6 @@ */ protected int stride; - /** * Provide an instance of Strided1DNIOBuffer with navigation variables initialised, * for sub-class use. The buffer ({@link #storage}, {@link #index0}), and the navigation ( @@ -49,8 +47,8 @@ *

* The sub-class constructor should check that the intended access is compatible with this * object by calling {@link #checkRequestFlags(int)}. (See the source of - * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, ByteBuffer, int, int, int)} - * for an example of this use.) + * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, ByteBuffer, int, int, int)} for + * an example of this use.) * * @param storage the ByteBuffer wrapping the exported object state. NOTE: this * PyBuffer keeps a reference and may manipulate the position, mark and @@ -64,11 +62,8 @@ */ protected Strided1DNIOBuffer(ByteBuffer storage, int index0, int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException { - super(storage, STRIDES); - this.index0 = index0; // Index to be treated as item[0] - shape = new int[]{count}; // Number of items in exported data + super(storage, STRIDES, index0, count, stride); this.stride = stride; // Between items - this.strides = new int[] {stride}; if (count == 0) { // Nothing to check as we'll make no accesses @@ -108,16 +103,14 @@ } } - /** + /** * Provide an instance of Strided1DNIOBuffer on a particular {@link ByteBuffer} - * specifying - * a starting index, the number of items in the result, and a byte-indexing stride. The result - * of byteAt(i) will be equal to storage.get(index0+stride*i) - * (whatever - * the sign of stride), valid for 0<=i<count. The constructor - * checks that all these indices lie within the storage (unless - * count=0). - * No reference will be kept to the ByteBuffer passed in. (It is duplicated.) + * specifying a starting index, the number of items in the result, and a byte-indexing stride. + * The result of byteAt(i) will be equal to + * storage.get(index0+stride*i) (whatever the sign of stride), valid + * for 0<=i<count. The constructor checks that all these indices lie within + * the storage (unless count=0). No reference will be kept to the + * ByteBuffer passed in. (It is duplicated.) *

* The constructed PyBuffer meets the consumer's expectations as expressed in the * flags argument, or an exception will be thrown if these are incompatible with @@ -149,11 +142,11 @@ return index0 + index * stride; } - /** + /** * {@inheritDoc} *

- * Strided1DNIOBuffer provides an implementation for slicing already-strided bytes in - * one dimension. In that case, x(i) = u(r+ip) for i = 0..L-1 where u is the + * Strided1DNIOBuffer provides an implementation for slicing already-strided bytes + * in one dimension. In that case, x(i) = u(r+ip) for i = 0..L-1 where u is the * underlying buffer, and r, p and L are the start, stride and count with * which x was created from u. Thus y(k) = u(r+sp+kmp), that is, the * composite index0 is r+sp and the composite stride is @@ -175,10 +168,9 @@ } } - /** - * A Strided1DNIOBuffer.SlicedView represents a non-contiguous subsequence of a simple - * buffer. + * A Strided1DNIOBuffer.SlicedView represents a non-contiguous subsequence of a + * simple buffer. */ static class SlicedView extends Strided1DNIOBuffer { @@ -192,14 +184,14 @@ * @param flags consumer requirements * @param storage ByteBuffer wrapping exported data (no reference kept) * @param index0 index into storage of item[0] - * @param len number of items in the slice + * @param count the number of items in the sliced view * @param stride in between successive elements of the new PyBuffer * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SlicedView(PyBuffer root, int flags, ByteBuffer storage, int index0, int len, int stride) - throws PyException { + public SlicedView(PyBuffer root, int flags, ByteBuffer storage, int index0, int count, + int stride) throws PyException { // Create a new slice on the buffer passed in (part of the root) - super(flags, storage, index0, len, stride); + super(flags, storage, index0, count, stride); // Get a lease on the root PyBuffer (read-only) this.root = root.getBuffer(FULL_RO); } 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 @@ -28,7 +28,7 @@ * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] * @param count number of items in the slice - * @param stride in between successive elements of the new PyBuffer + * @param stride byte-index distance from one element to the next in the new PyBuffer * @throws NullPointerException if storage is null * @throws ArrayIndexOutOfBoundsException if index0, count and * stride are inconsistent with storage.length @@ -100,14 +100,14 @@ * @param flags consumer requirements * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] - * @param len number of items in the slice + * @param count number of items in the sliced view * @param stride in between successive elements of the new PyBuffer * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int len, int stride) - throws PyException { + public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int count, + int stride) throws PyException { // Create a new on the buffer passed in (part of the root) - super(flags, storage, index0, len, stride); + super(flags, storage, index0, count, stride); // Get a lease on the root PyBuffer (writable) this.root = root.getBuffer(FULL); } diff --git a/src/org/python/core/buffer/ZeroByteBuffer.java b/src/org/python/core/buffer/ZeroByteBuffer.java --- a/src/org/python/core/buffer/ZeroByteBuffer.java +++ b/src/org/python/core/buffer/ZeroByteBuffer.java @@ -17,9 +17,6 @@ /** Shared instance of a zero-length storage. */ private static final byte[] EMPTY = new byte[0]; - /** Array containing a single zero for the length */ - protected static final int[] SHAPE = {0}; - /** * Construct an instance of a zero-length buffer, choosing whether it should report itself to be * read-only through {@link #isReadonly()} or as having a backing array through @@ -33,10 +30,7 @@ * @throws PyException (BufferError) when client expectations do not correspond with the type */ public ZeroByteBuffer(int flags, boolean readonly, boolean hasArray) throws PyException { - super(CONTIGUITY | (readonly ? 0 : WRITABLE)); - this.storage = EMPTY; // Empty array - this.shape = SHAPE; // {0} - this.strides = BaseBuffer.CONTIG_STRIDES; // {1} + super(EMPTY, CONTIGUITY | (readonly ? 0 : WRITABLE), 0, 0, 1); if (!hasArray) { // super() knows we have an array, but this truth is inconvenient here. removeFeatureFlags(AS_ARRAY); diff --git a/tests/java/org/python/core/PyBufferNIOTest.java b/tests/java/org/python/core/PyBufferNIOTest.java --- a/tests/java/org/python/core/PyBufferNIOTest.java +++ b/tests/java/org/python/core/PyBufferNIOTest.java @@ -7,7 +7,6 @@ import org.junit.runners.Parameterized.Parameters; import org.python.core.ByteBufferTestSupport.ByteMaterial; -import org.python.core.PyBufferTest.TestableExporter; import org.python.core.PyBufferTestSupport.ExporterFactory; import org.python.core.PyBufferTestSupport.TestSpec; import org.python.core.PyBufferTestSupport.WritableExporterFactory; @@ -249,11 +248,10 @@ int count, int stride) throws IndexOutOfBoundsException, NullPointerException, PyException { // Client will need to navigate using shape and strides if this is a slice - super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : PyBUF.STRIDES)); + super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : STRIDES), // + index0, new int[] {count}, new int[] {stride}); this.storage = storage.duplicate(); - this.index0 = index0; - shape = new int[] {count}; - strides = new int[] {stride}; + // Check the potential index range if (count > 0) { int end = index0 + (count - 1) * stride; 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 @@ -1304,11 +1304,9 @@ int count, int stride) throws IndexOutOfBoundsException, NullPointerException, PyException { // Client will need to navigate using shape and strides if this is a slice - super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : PyBUF.STRIDES)); + super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : STRIDES), // + index0, new int[] {count}, new int[] {stride}); this.storage = storage; - this.index0 = index0; - shape = new int[] {count}; - strides = new int[] {stride}; // Check the potential index range if (count > 0) { int end = index0 + (count - 1) * stride; -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:24 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:24 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Add_implementations_of_PyBu?= =?utf-8?q?ffer_based_on_java=2Enio=2EByteBuffer?= Message-ID: <20160827131224.63903.41018.55FFD710@psf.io> https://hg.python.org/jython/rev/369b40954d96 changeset: 7942:369b40954d96 user: Jeff Allen date: Sat Jun 11 07:39:53 2016 +0100 summary: Add implementations of PyBuffer based on java.nio.ByteBuffer Implementations and a test using the recent refactoring of the array- based buffer implementation. More refactoring and API change is pending, see comments for a start. files: src/org/python/core/buffer/BaseArrayBuffer.java | 4 +- src/org/python/core/buffer/BaseNIOBuffer.java | 345 ++++++++++ src/org/python/core/buffer/SimpleBuffer.java | 2 + src/org/python/core/buffer/SimpleNIOBuffer.java | 206 +++++ src/org/python/core/buffer/Strided1DNIOBuffer.java | 219 ++++++ tests/java/org/python/core/PyBufferNIOTest.java | 316 +++++++++ tests/java/org/python/core/PyBufferTest.java | 17 +- 7 files changed, 1097 insertions(+), 12 deletions(-) diff --git a/src/org/python/core/buffer/BaseArrayBuffer.java b/src/org/python/core/buffer/BaseArrayBuffer.java --- a/src/org/python/core/buffer/BaseArrayBuffer.java +++ b/src/org/python/core/buffer/BaseArrayBuffer.java @@ -9,8 +9,7 @@ /** * Base implementation of the Buffer API for when the storage implementation is byte[]. * The description of {@link BaseBuffer} mostly applies. Methods provided or overridden here are - * appropriate to 1-dimensional arrays backed by byte[]. - * + * appropriate to 1-dimensional arrays, of any item size, backed by byte[]. */ public abstract class BaseArrayBuffer extends BaseBuffer implements PyBuffer { @@ -275,5 +274,4 @@ } } } - } diff --git a/src/org/python/core/buffer/BaseNIOBuffer.java b/src/org/python/core/buffer/BaseNIOBuffer.java new file mode 100644 --- /dev/null +++ b/src/org/python/core/buffer/BaseNIOBuffer.java @@ -0,0 +1,345 @@ +package org.python.core.buffer; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ReadOnlyBufferException; + +import org.python.core.PyBUF; +import org.python.core.PyBuffer; +import org.python.core.PyException; + +/** + * Base implementation of the Buffer API for when the storage implementation is + * java.nio.ByteBuffer. The description of {@link BaseBuffer} mostly applies. Methods + * provided or overridden here are appropriate to 1-dimensional arrays, of any item size, backed by + * a ByteBuffer. + */ +public abstract class BaseNIOBuffer extends BaseBuffer implements PyBuffer { + + /** + * A {@link java.nio.ByteBuffer} (possibly a direct buffer) wrapping the storage that the + * exporter is sharing with the consumer. The data to be exposed may be only a subset of the + * bytes in the buffer, defined by the navigation information index0, + * shape, strides, etc., usually defined in the constructor. + *

+ * Implementations must not adjust the position and limit of storage after + * construction. It will generally be a duplicate of (not a reference to) a ByteBuffer held by + * the client code. The capacity and backing store are fixed in construction, and the position + * will always be {@link #index0}. The limit is always higher than any valid data, and in the + * case of a contiguous buffer (with positive stride), is exactly just beyond the last item, so + * that a series of ByteBuffer.get operations will yield the data. + */ + protected ByteBuffer storage; + + /** + * Partially construct an instance of BaseNIOBuffer in support of a sub-class, + * specifying the 'feature flags', or at least a starting set to be adjusted later. These are + * the features of the buffer exported, not the flags that form the consumer's request. The + * buffer will be read-only and/or backed by a (heap) array according to the properties of the + * ByteBuffer passed in. {@link PyBUF#FORMAT} is implicitly added to the feature + * flags. To complete initialisation, the sub-class normally must assign: {@link #index0}) and + * the navigation arrays ({@link #shape}, {@link #strides}), and call + * {@link #checkRequestFlags(int)} passing the consumer's request flags. + * + * @param storage the ByteBuffer wrapping the exported object state. NOTE: this + * PyBuffer keeps a reference and may manipulate the position, mark and + * limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy. + * @param featureFlags bit pattern that specifies the actual features allowed/required + */ + protected BaseNIOBuffer(ByteBuffer storage, int featureFlags) { + super(featureFlags & ~(WRITABLE | AS_ARRAY)); + this.storage = storage; + + // Deduce other feature flags from the client's ByteBuffer + if (!storage.isReadOnly()) { + addFeatureFlags(WRITABLE); + } + if (storage.hasArray()) { + addFeatureFlags(AS_ARRAY); + } + } + + @Override + protected int getSize() { + return shape[0]; + } + + @Override + public int getLen() { + return shape[0] * getItemsize(); + } + + @Override + protected byte byteAtImpl(int byteIndex) throws IndexOutOfBoundsException { + return storage.get(byteIndex); + } + + @Override + protected void storeAtImpl(byte value, int byteIndex) throws IndexOutOfBoundsException, + PyException { + // XXX consider catching ReadonlyBufferException instead of checking (and others: index?) + checkWritable(); + storage.put(byteIndex, value); + } + + @Override + protected int byteIndex(int... indices) throws IndexOutOfBoundsException { + // BaseBuffer implementation can be simplified since if indices.length!=1 we error. + checkDimension(indices.length); // throws if != 1 + return byteIndex(indices[0]); + } + + /** + * {@inheritDoc} + *

+ * Specialised to one-dimensional, possibly strided buffer. + */ + @Override + protected int calcGreatestIndex() { + int stride = strides[0]; + if (stride == 1) { + return index0 + shape[0] - 1; + } else if (stride > 0) { + return index0 + (shape[0] - 1) * stride; + } else { + return index0 - 1; + } + } + + /** + * {@inheritDoc} + *

+ * Specialised to one-dimensional, possibly strided buffer. + */ + @Override + protected int calcLeastIndex() { + int stride = strides[0]; + if (stride < 0) { + return index0 + (shape[0] - 1) * stride; + } else { + return index0; + } + } + + /** + * {@inheritDoc} + *

+ * The default implementation in BaseNIOBuffer deals with the general + * one-dimensional case of arbitrary item size and stride. + */ + @Override + public void copyTo(int srcIndex, byte[] dest, int destPos, int count) + throws IndexOutOfBoundsException { + // Wrap the destination, taking care to reflect the necessary range we shall write. + ByteBuffer destBuf = ByteBuffer.wrap(dest, destPos, count * getItemsize()); + copyTo(srcIndex, destBuf, count); + } + + /** + * {@inheritDoc} + *

+ * The default implementation in BaseBuffer deals with the general one-dimensional + * case of arbitrary item size and stride. + */ + // XXX Should this become part of the PyBUffer interface? + public void copyTo(ByteBuffer dest) throws BufferOverflowException, ReadOnlyBufferException, + PyException { + // Note shape[0] is the number of items in the buffer + copyTo(0, dest, shape[0]); + } + + /** + * {@inheritDoc} + *

+ * The default implementation in BaseNIOBuffer deals with the general + * one-dimensional case of arbitrary item size and stride. + */ + // XXX Should this become part of the PyBuffer interface? + protected void copyTo(int srcIndex, ByteBuffer dest, int count) throws BufferOverflowException, + ReadOnlyBufferException, IndexOutOfBoundsException, PyException { + + if (count > 0) { + + ByteBuffer src = getNIOByteBuffer(srcIndex); + + // Pick up attributes necessary to choose an efficient copy strategy + int itemsize = getItemsize(); + int stride = getStrides()[0]; + + // Strategy depends on whether items are laid end-to-end contiguously or there are gaps + if (stride == itemsize) { + // stride == itemsize: straight copy of contiguous bytes + src.limit(src.position() + count * itemsize); + dest.put(src); + + } else if (itemsize == 1) { + // Non-contiguous copy: single byte items + int pos = src.position(); + for (int i = 0; i < count; i++) { + src.position(pos); + dest.put(src.get()); + pos += stride; + } + + } else { + // Non-contiguous copy: each time, copy itemsize bytes then skip + int pos = src.position(); + for (int i = 0; i < count; i++) { + src.limit(pos + itemsize).position(pos); + dest.put(src); + pos += stride; + } + } + } + } + + /** + * {@inheritDoc} + *

+ * The default implementation in BaseNIOBuffer deals with the general + * one-dimensional case of arbitrary item size and stride. + */ + @Override + public void copyFrom(byte[] src, int srcPos, int destIndex, int count) + throws IndexOutOfBoundsException, PyException { + // Wrap the source, taking care to reflect the range we shall read. + ByteBuffer srcBuf = ByteBuffer.wrap(src, srcPos, count * getItemsize()); + copyFrom(srcBuf, destIndex, count); + } + + /** + * {@inheritDoc} + *

+ * The default implementation in BaseNIOBuffer deals with the general + * one-dimensional case of arbitrary item size and stride. + */ + // XXX Should this become part of the PyBUffer interface? + protected void copyFrom(ByteBuffer src, int dstIndex, int count) + throws IndexOutOfBoundsException, PyException { + + checkWritable(); + + if (count > 0) { + + ByteBuffer dst = getNIOByteBuffer(dstIndex); + + // Pick up attributes necessary to choose an efficient copy strategy + int itemsize = getItemsize(); + int stride = getStrides()[0]; + int skip = stride - itemsize; + + // Strategy depends on whether items are laid end-to-end or there are gaps + if (skip == 0) { + // Straight copy of contiguous bytes + dst.put(src); + + } else if (itemsize == 1) { + // Non-contiguous copy: single byte items + int pos = dst.position(); + for (int i = 0; i < count; i++) { + dst.position(pos); + dst.put(src.get()); + // Next byte written will be here + pos += stride; + } + + } else { + // Non-contiguous copy: each time, copy itemsize bytes at a time + int pos = dst.position(); + for (int i = 0; i < count; i++) { + dst.position(pos); + // Delineate the next itemsize bytes in the src + src.limit(src.position() + itemsize); + dst.put(src); + // Next byte written will be here + pos += stride; + } + } + } + } + + /** + * {@inheritDoc} + *

+ * The default implementation in BaseNIOBuffer deals with the general + * one-dimensional case. + */ + @Override + public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { + + int length = getLen(); + int itemsize = getItemsize(); + + // Valid operation only if writable and same length and itemsize + checkWritable(); + if (src.getLen() != length || src.getItemsize() != itemsize) { + throw differentStructure(); + } + + if (length > 0) { + // Pick up attributes necessary to choose an efficient copy strategy + int stride = getStrides()[0]; + int skip = stride - itemsize; + + ByteBuffer dst = getNIOByteBuffer(); + + // Strategy depends on whether destination items are laid end-to-end or there are gaps + if (skip == 0) { + // Straight copy to contiguous bytes + for (int i = 0; i < length; i++) { + dst.put(src.byteAt(i)); + } + + } else if (itemsize == 1) { + // Non-contiguous copy: single byte items + int pos = dst.position(); + for (int i = 0; i < length; i++) { + dst.put(pos, src.byteAt(i)); + pos += stride; + } + + } else { + // Non-contiguous copy: each time, and itemsize > 1 + int pos = dst.position(); + int s = 0; + for (int i = 0; i < length; i++) { + for (int j = 0; j < itemsize; j++) { + dst.put(pos++, src.byteAt(s++)); + } + pos += skip; + } + } + } + + } + + @Override + protected ByteBuffer getNIOByteBufferImpl() { + return storage.duplicate(); + } + + @SuppressWarnings("deprecation") + @Override + public Pointer getBuf() { + checkHasArray(); + return new Pointer(storage.array(), index0); + } + + /** + * {@inheritDoc} + *

+ * Specialised in BaseArrayBuffer to one dimension. + */ + @Override + public boolean isContiguous(char order) { + if ("CFA".indexOf(order) < 0) { + return false; + } else { + if (getShape()[0] < 2) { + return true; + } else { + return getStrides()[0] == getItemsize(); + } + } + } +} diff --git a/src/org/python/core/buffer/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 @@ -131,6 +131,8 @@ return index0 + index; } + // XXX Consider moving to clauses in getBufferSlice(int, int, int, int) + // to avoid delegation loop where that delegates to this but in BaseBuffer the reverse. @Override public PyBuffer getBufferSlice(int flags, int start, int count) { if (count > 0) { diff --git a/src/org/python/core/buffer/SimpleNIOBuffer.java b/src/org/python/core/buffer/SimpleNIOBuffer.java new file mode 100644 --- /dev/null +++ b/src/org/python/core/buffer/SimpleNIOBuffer.java @@ -0,0 +1,206 @@ +package org.python.core.buffer; + +import java.nio.ByteBuffer; + +import org.python.core.PyBuffer; +import org.python.core.PyException; + +/** + * Buffer API over a read-only one-dimensional array of one-byte items. + */ + +public class SimpleNIOBuffer extends BaseNIOBuffer { + + /** + * The strides array for this type is always a single element array with a 1 in it. + */ + protected static final int[] SIMPLE_STRIDES = {1}; // XXX Push up? + + /** + * Provide an instance of SimpleNIOBuffer with navigation variables initialised, + * for sub-class use. The buffer ({@link #storage}, {@link #index0}), and the {@link #shape} + * array will be initialised from the arguments (which are checked for range). The + * {@link #strides} is set for (one-byte) unit stride. Only the call to + * {@link #checkRequestFlags(int)}, passing the consumer's request flags, really remains for the + * sub-class constructor to do. + * + *

+     * super(storage.duplicate(), index0, size);
+     * checkRequestFlags(flags);        // Check request is compatible with type
+     * 
+ * + * @param storage the ByteBuffer wrapping the exported object state. NOTE: this + * PyBuffer keeps a reference and may manipulate the position, mark and + * limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy. + * @param index0 offset where the data starts in that array (item[0]) + * @param size the number of bytes occupied + * @throws NullPointerException if storage is null + * @throws ArrayIndexOutOfBoundsException if index0 and size are + * inconsistent with storage.capacity() + */ + protected SimpleNIOBuffer(ByteBuffer storage, int index0, int size) throws PyException, + ArrayIndexOutOfBoundsException { + super(storage, CONTIGUITY | SIMPLE); + + // Initialise navigation + shape = new int[] {size}; // Number of items in exported data + strides = SIMPLE_STRIDES; + // suboffsets is always null for this type. + + this.storage = storage; // Exported data + this.index0 = index0; // Index to be treated as item[0] + + // Check arguments using the "all non-negative" trick + if ((index0 | size | storage.capacity() - (index0 + size)) < 0) { + throw new ArrayIndexOutOfBoundsException(); + } + } + + /** + * Provide an instance of SimpleNIOBuffer, on a slice of a {@link ByteBuffer}, + * meeting the consumer's expectations as expressed in the flags argument, which is + * checked against the capabilities of the buffer type. No reference will be kept to the + * ByteBuffer passed in. (It is duplicated.) + * + * @param flags consumer requirements + * @param storage the ByteBuffer wrapping the exported object state + * @param index0 offset where the data starts in that buffer (item[0]) + * @param size the number of bytes occupied + * @throws NullPointerException if storage is null + * @throws ArrayIndexOutOfBoundsException if index0 and size are + * inconsistent with storage.length + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public SimpleNIOBuffer(int flags, ByteBuffer storage, int index0, int size) throws PyException, + ArrayIndexOutOfBoundsException, NullPointerException { + this(storage.duplicate(), index0, size); // Construct checked SimpleNIOBuffer + checkRequestFlags(flags); // Check request is compatible with type + } + + /** + * Provide an instance of SimpleNIOBuffer, on the entirety of a {@link ByteBuffer}, + * with navigation variables initialised, for sub-class use. The buffer ( {@link #storage}, + * {@link #index0}), and the navigation ({@link #shape} array) will be initialised from the + * argument. + * + * @param storage the ByteBuffer wrapping the exported object state. NOTE: this + * PyBuffer keeps a reference and may manipulate the position, mark and + * limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy. + * @throws NullPointerException if storage is null + */ + protected SimpleNIOBuffer(ByteBuffer storage) throws NullPointerException { + this(storage, 0, storage.capacity()); + } + + /** + * Provide an instance of SimpleNIOBuffer, on the entirety of a {@link ByteBuffer}, + * meeting the consumer's expectations as expressed in the flags argument, which is + * checked against the capabilities of the buffer type. No reference will be kept to the + * ByteBuffer passed in. (It is duplicated.) + * + * @param flags consumer requirements + * @param storage the ByteBuffer wrapping the exported object state + * @throws NullPointerException if storage is null + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public SimpleNIOBuffer(int flags, ByteBuffer storage) throws PyException, NullPointerException { + this(storage.duplicate()); // Construct SimpleNIOBuffer on whole ByteBuffer + checkRequestFlags(flags); // Check request is compatible with type + } + + /** + * {@inheritDoc} + *

+ * SimpleNIOBuffer provides an implementation optimised for contiguous bytes in + * one-dimension. + */ + @Override + public int getLen() { + // Simplify for one-dimensional contiguous bytes + return shape[0]; + } + + @Override + protected int byteIndex(int index) throws IndexOutOfBoundsException { + return index0 + index; + } + + // XXX Consider moving to clauses in getBufferSlice(int, int, int, int) + // to avoid delegation loop where that delegates to this but in BaseBuffer the reverse. + @Override + public PyBuffer getBufferSlice(int flags, int start, int count) { + if (count > 0) { + // Translate relative to underlying buffer + int compIndex0 = index0 + start; + // Create the slice from the sub-range of the buffer + return new SimpleView(getRoot(), flags, storage, compIndex0, count); + } else { + // Special case for count==0 where above logic would fail. Efficient too. + return new ZeroByteBuffer.View(getRoot(), flags); + } + } + + /** + * {@inheritDoc} + *

+ * SimpleNIOBuffer provides an implementation for slicing contiguous bytes in one + * dimension. In that case, x(i) = u(r+i) for i = 0..L-1 where u is the underlying + * buffer, and r and L are the start and count with which x was created + * from u. Thus y(k) = u(r+s+km), that is, the composite offset is r+s and + * the stride is m. + */ + @Override + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { + + if (stride == 1 || count < 2) { + // Unstrided slice of simple buffer is special case + return getBufferSlice(flags, start, count); + + } else { + // Translate relative to underlying buffer + int compIndex0 = index0 + start; + // Construct a view, taking a lock on the root object (this or this.root) + return new Strided1DNIOBuffer.SlicedView(getRoot(), flags, storage, compIndex0, count, + stride); + } + } + + /** + * A SimpleNIOBuffer.SimpleView represents a contiguous subsequence of another + * SimpleNIOBuffer. + */ + static class SimpleView extends SimpleNIOBuffer { + + /** The buffer on which this is a slice view */ + PyBuffer root; + + /** + * Construct a slice of a SimpleNIOBuffer. + * + * @param root buffer which will be acquired and must be released ultimately + * @param flags the request flags of the consumer that requested the slice + * @param storage ByteBuffer wrapping exported data (no reference kept) + * @param offset where the data starts in that buffer (item[0]) + * @param size the number of bytes occupied + */ + public SimpleView(PyBuffer root, int flags, ByteBuffer storage, int offset, int size) { + // Create a new SimpleNIOBuffer on the buffer passed in (part of the root) + super(flags, storage, offset, size); + // 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/buffer/Strided1DNIOBuffer.java b/src/org/python/core/buffer/Strided1DNIOBuffer.java new file mode 100644 --- /dev/null +++ b/src/org/python/core/buffer/Strided1DNIOBuffer.java @@ -0,0 +1,219 @@ +package org.python.core.buffer; + +import java.nio.ByteBuffer; + +import org.python.core.PyBuffer; +import org.python.core.PyException; + + +/** + * Read-only buffer API over a one-dimensional array of one-byte items, that are evenly-spaced in a + * storage array. The buffer has storage, index0 and length + * properties in the usual way, designating a slice (or all) of a byte array, but also a + * stride property (equal to getStrides()[0]). + *

+ * Let the underlying buffer be the byte array u(i) for i=0..N-1, let x be the + * Strided1DNIOBuffer, and let the stride be p. The storage works as follows. + * Designate by x(j), for j=0..L-1, the byte at index j, that is, the byte + * retrieved by x.byteAt(j). Thus, we store x(j) at u(a+pj), that is, + * x(0) = u(a). When we construct such a buffer, we have to supply a = + * index0, L = count, and p = stride as the + * constructor arguments. The last item in the slice x(L-1) is stored at u(a+p(L-1)). + * For the simple case of positive stride, constructor argument index0 is the low index + * of the range occupied by the data. When the stride is negative, that is to say p<0, and + * L>1, this will be to the left of u(a), and the constructor argument + * index0 is not then the low index of the range occupied by the data. Clearly both + * these indexes must be in the range 0 to N-1 inclusive, a rule enforced by the constructors + * (unless L=0, when it is assumed no array access will take place). + *

+ * The class may be used by exporters to create a strided slice (e.g. to export the diagonal of a + * matrix) and in particular by other buffers to create strided slices of themselves, such as to + * create the memoryview that is returned as an extended slice of a + * memoryview. + */ +public class Strided1DNIOBuffer extends BaseNIOBuffer { + + /** + * Step size in the underlying buffer essential to correct translation of an index (or indices) + * into an index into the storage. The value is returned by {@link #getStrides()} is an array + * with this as the only element. + */ + protected int stride; + + + /** + * Provide an instance of Strided1DNIOBuffer with navigation variables initialised, + * for sub-class use. The buffer ({@link #storage}, {@link #index0}), and the navigation ( + * {@link #shape} array and {@link #stride}) will be initialised from the arguments (which are + * checked for range). + *

+ * The sub-class constructor should check that the intended access is compatible with this + * object by calling {@link #checkRequestFlags(int)}. (See the source of + * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, ByteBuffer, int, int, int)} + * for an example of this use.) + * + * @param storage the ByteBuffer wrapping the exported object state. NOTE: this + * PyBuffer keeps a reference and may manipulate the position, mark and + * limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy. + * @param index0 index into storage of item[0] + * @param count number of items in the slice + * @param stride in between successive elements of the new PyBuffer + * @throws NullPointerException if storage is null + * @throws ArrayIndexOutOfBoundsException if index0, count and + * stride are inconsistent with storage.length + */ + protected Strided1DNIOBuffer(ByteBuffer storage, int index0, int count, int stride) + throws ArrayIndexOutOfBoundsException, NullPointerException { + super(storage, STRIDES); + this.index0 = index0; // Index to be treated as item[0] + shape = new int[]{count}; // Number of items in exported data + this.stride = stride; // Between items + this.strides = new int[] {stride}; + + if (count == 0) { + // Nothing to check as we'll make no accesses + addFeatureFlags(CONTIGUITY); + + } else { + // Need to check lowest and highest index against array + int lo, hi; + + if (stride == 1) { + lo = index0; // First byte of item[0] + hi = index0 + count; // Last byte of item[L-1] + 1 + addFeatureFlags(CONTIGUITY); + + } else if (stride > 1) { + lo = index0; // First byte of item[0] + hi = index0 + (count - 1) * stride + 1; // Last byte of item[L-1] + 1 + + } else { + hi = index0 + 1; // Last byte of item[0] + 1 + lo = index0 + (count - 1) * stride; // First byte of item[L-1] + } + + // Check indices using "all non-negative" trick + int cap = storage.capacity(); + if ((count | lo | (cap - lo) | hi | (cap - hi)) < 0) { + throw new ArrayIndexOutOfBoundsException(); + } + } + + // Deduce feature flags from the client's ByteBuffer + if (!storage.isReadOnly()) { + addFeatureFlags(WRITABLE); + } + if (storage.hasArray()) { + addFeatureFlags(AS_ARRAY); + } + } + + /** + * Provide an instance of Strided1DNIOBuffer on a particular {@link ByteBuffer} + * specifying + * a starting index, the number of items in the result, and a byte-indexing stride. The result + * of byteAt(i) will be equal to storage.get(index0+stride*i) + * (whatever + * the sign of stride), valid for 0<=i<count. The constructor + * checks that all these indices lie within the storage (unless + * count=0). + * No reference will be kept to the ByteBuffer passed in. (It is duplicated.) + *

+ * The constructed PyBuffer meets the consumer's expectations as expressed in the + * flags argument, or an exception will be thrown if these are incompatible with + * the type (e.g. the consumer does not specify that it understands the strides array). Note + * that the actual range in the storage array, the lowest and highest index, is not + * explicitly passed, but is implicit in index0, count and + * stride. The constructor checks that these indices lie within the + * storage array (unless count=0). + * + * @param flags consumer requirements + * @param storage ByteBuffer wrapping exported data + * @param index0 index into storage of item[0] + * @param count number of items in the slice + * @param stride in between successive elements of the new PyBuffer + * @throws NullPointerException if storage is null + * @throws ArrayIndexOutOfBoundsException if index0, count and + * stride are inconsistent with storage.length + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public Strided1DNIOBuffer(int flags, ByteBuffer storage, int index0, int count, int stride) + throws ArrayIndexOutOfBoundsException, NullPointerException, PyException { + this(storage.duplicate(), index0, count, stride); + checkRequestFlags(flags); // Check request is compatible with type + + } + + @Override + protected int byteIndex(int index) throws IndexOutOfBoundsException { + return index0 + index * stride; + } + + /** + * {@inheritDoc} + *

+ * Strided1DNIOBuffer provides an implementation for slicing already-strided bytes in + * one dimension. In that case, x(i) = u(r+ip) for i = 0..L-1 where u is the + * underlying buffer, and r, p and L are the start, stride and count with + * which x was created from u. Thus y(k) = u(r+sp+kmp), that is, the + * composite index0 is r+sp and the composite stride is + * mp. + */ + @Override + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { + + if (count > 0) { + // Translate start relative to underlying buffer + int compStride = this.stride * stride; + int compIndex0 = index0 + start * this.stride; + // Construct a view, taking a lock on the root object (this or this.root) + return new SlicedView(getRoot(), flags, storage, compIndex0, count, compStride); + + } else { + // Special case for count==0 where above logic would fail. Efficient too. + return new ZeroByteBuffer.View(getRoot(), flags); + } + } + + + /** + * A Strided1DNIOBuffer.SlicedView represents a non-contiguous subsequence of a simple + * buffer. + */ + static class SlicedView extends Strided1DNIOBuffer { + + /** The buffer on which this is a slice view */ + PyBuffer root; + + /** + * Construct a slice of a one-dimensional byte buffer. + * + * @param root on which release must be called when this is released + * @param flags consumer requirements + * @param storage ByteBuffer wrapping exported data (no reference kept) + * @param index0 index into storage of item[0] + * @param len number of items in the slice + * @param stride in between successive elements of the new PyBuffer + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public SlicedView(PyBuffer root, int flags, ByteBuffer storage, int index0, int len, int stride) + throws PyException { + // Create a new slice on the buffer passed in (part of the root) + super(flags, storage, index0, len, stride); + // Get a lease on the root PyBuffer (read-only) + 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/tests/java/org/python/core/PyBufferNIOTest.java b/tests/java/org/python/core/PyBufferNIOTest.java new file mode 100644 --- /dev/null +++ b/tests/java/org/python/core/PyBufferNIOTest.java @@ -0,0 +1,316 @@ +package org.python.core; + +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.List; + +import org.junit.runners.Parameterized.Parameters; +import org.python.core.ByteBufferTestSupport.ByteMaterial; +import org.python.core.PyBufferTest.TestableExporter; +import org.python.core.PyBufferTestSupport.ExporterFactory; +import org.python.core.PyBufferTestSupport.TestSpec; +import org.python.core.PyBufferTestSupport.WritableExporterFactory; +import org.python.core.buffer.BaseBuffer; +import org.python.core.buffer.SimpleNIOBuffer; + +public class PyBufferNIOTest extends PyBufferTest { + + public PyBufferNIOTest(TestSpec spec) { + super(spec); + } + + /** + * Generate test data to be held in the testing framework and used to construct tests. This + * method is called once by the test framework. Each element of the returned collection is a + * specification that becomes the arguments to the constructor when JUnit prepares to invoke a + * test. + *

+ * Internally, this method creates a small number of instances of the object types whose + * PyBuffer export mechanism is to be tested. Each is paired with a reference value + * represented in several forms. The PyBufferTestSupport class then multiplies + * these by creating a selection of feasible sliced views, the whole collection of root and + * slice objects being returned. + * + * @return generated list of test data + */ + @Parameters + public static Collection genTestSpecs() { + + PyBufferTestSupport s = new PyBufferTestSupport(sliceLengths, sliceSteps); + + // Tests using local types of exporter + + ExporterFactory rollYourOwnExporter = new WritableExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new RollYourOwnExporter(m.getBuffer()); + } + + }; + s.add(rollYourOwnExporter, byteMaterial); + s.add(rollYourOwnExporter, emptyMaterial); + + // All combinations of heap/direct, writable and empty/small/large (I'm so thorough!) + + ExporterFactory readonlyHeapNIOExporter = new TestNIOExporterFactory(false, false); + s.add(readonlyHeapNIOExporter, emptyMaterial); + s.add(readonlyHeapNIOExporter, byteMaterial); + s.add(readonlyHeapNIOExporter, longMaterial); + + ExporterFactory writableHeapNIOExporter = new TestNIOExporterFactory(true, false); + s.add(writableHeapNIOExporter, emptyMaterial); + s.add(writableHeapNIOExporter, byteMaterial); + s.add(writableHeapNIOExporter, longMaterial); + + ExporterFactory readonlyDirectNIOExporter = new TestNIOExporterFactory(false, true); + s.add(readonlyDirectNIOExporter, emptyMaterial); + s.add(readonlyDirectNIOExporter, byteMaterial); + s.add(readonlyDirectNIOExporter, longMaterial); + + ExporterFactory writableDirectNIOExporter = new TestNIOExporterFactory(true, true); + s.add(writableDirectNIOExporter, emptyMaterial); + s.add(writableDirectNIOExporter, byteMaterial); + s.add(writableDirectNIOExporter, longMaterial); + + // Return the generated test data + + List ret = s.getTestData(); + if (PRINT_KEY) { + int key = 0; + for (TestSpec[] r : ret) { + TestSpec spec = r[0]; + System.out.printf("%6d : %s\n", key++, spec.toString()); + } + } + return ret; + } + + /* + * -------------------------------------------------------------------------------------------- + * A series of custom exporters that use a java.nio.ByteBuffer to store and export their + * implementation data. + * -------------------------------------------------------------------------------------------- + */ + /** + * A class to act as an exporter that uses the SimpleBuffer. The exporter shares a single + * exported buffer between all consumers and needs to take any action immediately when that + * buffer is finally released. You are most likely to use this approach with an exporting object + * type that modifies its behaviour while there are active exports, but where it is worth + * avoiding the cost of duplicate buffers. This is the case with PyByteArray, which prohibits + * operations that would resize it, while there are outstanding exports. + */ + private static class TestNIOExporter extends TestableExporter { + + protected ByteBuffer storage; + + /** + * Construct a simple exporter from the bytes supplied. + * + * @param storage + */ + public TestNIOExporter(ByteBuffer storage) { + this.storage = storage; + } + + @Override + public PyBuffer getBuffer(int flags) { + // If we have already exported a buffer it may still be available for re-use + BaseBuffer pybuf = getExistingBuffer(flags); + if (pybuf == null) { + // No existing export we can re-use + pybuf = new SimpleNIOBuffer(flags, storage) { + + @Override + protected void releaseAction() { + export = null; // Final release really is final (not reusable) + } + }; + + // Hold a reference for possible re-use + export = new WeakReference(pybuf); + } + return pybuf; + } + + } + + /** + * A factory for exporting objects to be used in the tests. These objects use a + * ByteBuffer for their exported representation, and the factory is programmed on + * creation to whether these buffers should be writable or direct. + */ + static class TestNIOExporterFactory implements ExporterFactory { + + final boolean writable; + final boolean isDirect; + + TestNIOExporterFactory(boolean writable, boolean isDirect) { + this.writable = writable; + this.isDirect = isDirect; + } + + public boolean isWritable() { + return writable; + } + + public boolean isDirect() { + return isDirect; + } + + @Override + public BufferProtocol make(ByteMaterial m) { + ByteBuffer bb = m.getBuffer(); + if (isDirect) { + // Replace bb with a direct buffer containing the same bytes + ByteBuffer direct = ByteBuffer.allocateDirect(bb.capacity()); + direct.put(bb).flip(); + bb = direct; + } + if (!writable) { + bb = bb.asReadOnlyBuffer(); + } + return new TestNIOExporter(bb); + } + + @Override + public boolean isReadonly() { + return !writable; + } + + @Override + public boolean hasArray() { + return !isDirect && writable; + } + + } + + /** A class to act as an exporter that uses the RollYourOwnNIOBuffer class. */ + private static class RollYourOwnExporter extends TestableExporter { + + protected ByteBuffer storage; + + public RollYourOwnExporter(ByteBuffer storage) { + this.storage = storage; + } + + @Override + public PyBuffer getBuffer(int flags) { + // If we have already exported a buffer it may still be available for re-use + BaseBuffer pybuf = getExistingBuffer(flags); + if (pybuf == null) { + // No existing export we can re-use + pybuf = new RollYourOwnNIOBuffer(flags, storage); + // Hold a reference for possible re-use + export = new WeakReference(pybuf); + } + return pybuf; + } + + } + + /** + * Minimal extension of BaseBuffer in order to test the default implementations there. They're + * slow, so mostly we override them in the implementations BaseArrayBuffer and BaseNIOBuffer, + * but they still have to be correct. The class represents a one-dimensional, strided array of + * bytes, so it can represent a slice of itself. + */ + private static class RollYourOwnNIOBuffer extends BaseBuffer { + + final static int FEATURES = PyBUF.WRITABLE | PyBUF.AS_ARRAY; + + final ByteBuffer storage; + final PyBuffer root; + + /** + * Create a buffer view of a given ByteBuffer in which the data is the + * contiguous sequence of bytes from the position to the limit. + * + * @param flags consumer requirements + * @param storage buffer exported (from the position to the limit) + */ + public RollYourOwnNIOBuffer(int flags, ByteBuffer storage) { + this(null /* =this */, flags, storage, storage.position(), storage.remaining(), 1); + } + + /** + * Construct a slice of a one-dimensional byte buffer. + * + * @param root on which release must be called when this is released + * @param flags consumer requirements + * @param storage buffer containing exported data + * @param index0 index into storage of item[0] + * @param count number of items in the slice + * @param stride in between successive elements of the new PyBuffer + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public RollYourOwnNIOBuffer(PyBuffer root, int flags, ByteBuffer storage, int index0, + int count, int stride) throws IndexOutOfBoundsException, NullPointerException, + PyException { + // Client will need to navigate using shape and strides if this is a slice + super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : PyBUF.STRIDES)); + this.storage = storage.duplicate(); + this.index0 = index0; + shape = new int[] {count}; + strides = new int[] {stride}; + // Check the potential index range + if (count > 0) { + int end = index0 + (count - 1) * stride; + final int END = storage.capacity() - 1; + if (index0 < 0 || index0 > END || end < 0 || end > END) { + throw new IndexOutOfBoundsException(); + } + } + // Check client is compatible + checkRequestFlags(flags); + // Get a lease on the root PyBuffer (read-only). Last in case a check above fails. + if (root == null) { + this.root = this; + } else { + this.root = root.getBuffer(FULL_RO); + } + } + + @Override + protected PyBuffer getRoot() { + return root; + } + + @Override + public void releaseAction() { + // XXX Consider making this automatic within BaseBuffer.release() when getRoot()!=this + /* + * ... so that {@link #release()} takes care of this: sub-classes should not propagate + * the release themselves when overriding {@link #releaseAction()}. + */ + // We have to release the root too if ours was final and we are not that root. + if (root != this) { + root.release(); + } + } + + @Override + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { + int newStart = index0 + start * strides[0]; + int newStride = strides[0] * stride; + return new RollYourOwnNIOBuffer(root, flags, storage, newStart, count, newStride); + } + + @Override + public ByteBuffer getNIOByteBufferImpl() { + return storage.duplicate(); + } + + @Override + protected byte byteAtImpl(int byteIndex) { + return storage.get(byteIndex); + } + + @Override + protected void storeAtImpl(byte value, int byteIndex) throws IndexOutOfBoundsException, + PyException { + storage.put(byteIndex, value); + } + } +} diff --git a/tests/java/org/python/core/PyBufferTest.java b/tests/java/org/python/core/PyBufferTest.java --- a/tests/java/org/python/core/PyBufferTest.java +++ b/tests/java/org/python/core/PyBufferTest.java @@ -1242,7 +1242,7 @@ } - /** A class to act as an exporter that uses the RollYourOwnBuffer. */ + /** A class to act as an exporter that uses the RollYourOwnArrayBuffer class. */ private static class RollYourOwnExporter extends TestableExporter { protected byte[] storage; @@ -1290,28 +1290,28 @@ } /** - * Construct a slice of a one-dimensional byte buffer. + * Construct a slice of a one-dimensional byte array. * * @param root on which release must be called when this is released * @param flags consumer requirements * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] - * @param length number of items in the slice + * @param count number of items in the slice * @param stride in between successive elements of the new PyBuffer * @throws PyException (BufferError) when expectations do not correspond with the type */ public RollYourOwnArrayBuffer(PyBuffer root, int flags, byte[] storage, int index0, - int length, int stride) throws ArrayIndexOutOfBoundsException, - NullPointerException, PyException { + int count, int stride) throws IndexOutOfBoundsException, NullPointerException, + PyException { // Client will need to navigate using shape and strides if this is a slice super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : PyBUF.STRIDES)); this.storage = storage; this.index0 = index0; - shape = new int[] {length}; + shape = new int[] {count}; strides = new int[] {stride}; // Check the potential index range - if (length > 0) { - int end = index0 + (length - 1) * stride; + if (count > 0) { + int end = index0 + (count - 1) * stride; final int END = storage.length - 1; if (index0 < 0 || index0 > END || end < 0 || end > END) { throw new IndexOutOfBoundsException(); @@ -1367,6 +1367,5 @@ PyException { storage[byteIndex] = value; } - } } -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:25 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:25 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_BaseBuffer_implementation_t?= =?utf-8?q?akes_care_of_cascading_release=28=29=2E?= Message-ID: <20160827131225.18237.1860.EB342CD1@psf.io> https://hg.python.org/jython/rev/4a92b336647a changeset: 7947:4a92b336647a user: Jeff Allen date: Thu Jun 30 12:45:54 2016 +0100 summary: BaseBuffer implementation takes care of cascading release(). No longer have to override releaseAction() to cascade a release notification from a sliced view of a PyBuffer. files: src/org/python/core/buffer/BaseBuffer.java | 11 +++++- src/org/python/core/buffer/SimpleBuffer.java | 9 ----- src/org/python/core/buffer/SimpleNIOBuffer.java | 8 ---- src/org/python/core/buffer/SimpleStringBuffer.java | 7 ---- src/org/python/core/buffer/SimpleWritableBuffer.java | 8 ---- src/org/python/core/buffer/Strided1DBuffer.java | 9 ----- src/org/python/core/buffer/Strided1DNIOBuffer.java | 7 ---- src/org/python/core/buffer/Strided1DWritableBuffer.java | 8 ---- src/org/python/core/buffer/ZeroByteBuffer.java | 6 --- tests/java/org/python/core/PyBufferNIOTest.java | 13 ------- tests/java/org/python/core/PyBufferTest.java | 17 +-------- 11 files changed, 12 insertions(+), 91 deletions(-) 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 @@ -580,13 +580,22 @@ * equals the number of getBuffer calls), the implementation here calls * {@link #releaseAction()}, which the implementer of a specific buffer type should override if * it needs specific actions to take place. + *

+ * Note that, when this is a sliced view obtained from another PyBuffer the + * implementation in BaseBuffer automatically sends one release() + * Sub-classes should not propagate the release themselves when overriding + * {@link #releaseAction()}. */ @Override public void release() { if (--exports == 0) { // This is a final release. releaseAction(); - // XXX Consider adding release of root if root!=this so sliced need not + // We have to release the root too if we are not a root. + PyBuffer root = getRoot(); + if (root != this) { + root.release(); + } } else if (exports < 0) { // Buffer already had 0 exports. (Put this right, in passing.) exports = 0; 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 @@ -9,7 +9,6 @@ */ public class SimpleBuffer extends BaseArrayBuffer { - /** * Provide an instance of SimpleBuffer with navigation variables initialised, for * sub-class use. The buffer ({@link #storage}, {@link #index0}), and the {@link #shape} array @@ -199,13 +198,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } - } - } diff --git a/src/org/python/core/buffer/SimpleNIOBuffer.java b/src/org/python/core/buffer/SimpleNIOBuffer.java --- a/src/org/python/core/buffer/SimpleNIOBuffer.java +++ b/src/org/python/core/buffer/SimpleNIOBuffer.java @@ -185,13 +185,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } - } - } diff --git a/src/org/python/core/buffer/SimpleStringBuffer.java b/src/org/python/core/buffer/SimpleStringBuffer.java --- a/src/org/python/core/buffer/SimpleStringBuffer.java +++ b/src/org/python/core/buffer/SimpleStringBuffer.java @@ -214,12 +214,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } - } } diff --git a/src/org/python/core/buffer/SimpleWritableBuffer.java b/src/org/python/core/buffer/SimpleWritableBuffer.java --- a/src/org/python/core/buffer/SimpleWritableBuffer.java +++ b/src/org/python/core/buffer/SimpleWritableBuffer.java @@ -130,13 +130,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } - } - } diff --git a/src/org/python/core/buffer/Strided1DBuffer.java b/src/org/python/core/buffer/Strided1DBuffer.java --- a/src/org/python/core/buffer/Strided1DBuffer.java +++ b/src/org/python/core/buffer/Strided1DBuffer.java @@ -208,14 +208,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // XXX Consider making this automatic within BaseBuffer.release() when getRoot()!=this - // We have to release the root too if ours was final. - root.release(); - } - } - } diff --git a/src/org/python/core/buffer/Strided1DNIOBuffer.java b/src/org/python/core/buffer/Strided1DNIOBuffer.java --- a/src/org/python/core/buffer/Strided1DNIOBuffer.java +++ b/src/org/python/core/buffer/Strided1DNIOBuffer.java @@ -200,12 +200,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } - } } diff --git a/src/org/python/core/buffer/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 @@ -116,13 +116,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } - } - } diff --git a/src/org/python/core/buffer/ZeroByteBuffer.java b/src/org/python/core/buffer/ZeroByteBuffer.java --- a/src/org/python/core/buffer/ZeroByteBuffer.java +++ b/src/org/python/core/buffer/ZeroByteBuffer.java @@ -184,11 +184,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } } } diff --git a/tests/java/org/python/core/PyBufferNIOTest.java b/tests/java/org/python/core/PyBufferNIOTest.java --- a/tests/java/org/python/core/PyBufferNIOTest.java +++ b/tests/java/org/python/core/PyBufferNIOTest.java @@ -276,19 +276,6 @@ } @Override - public void releaseAction() { - // XXX Consider making this automatic within BaseBuffer.release() when getRoot()!=this - /* - * ... so that {@link #release()} takes care of this: sub-classes should not propagate - * the release themselves when overriding {@link #releaseAction()}. - */ - // We have to release the root too if ours was final and we are not that root. - if (root != this) { - root.release(); - } - } - - @Override public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { int newStart = index0 + start * strides[0]; int newStride = strides[0] * stride; 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 @@ -945,8 +945,8 @@ } else if (subject instanceof PyByteArray) { // Size-changing access should fail try { - ((PyByteArray)subject).bytearray_extend(Py.One); // Appends one zero byte - fail("bytearray_extend with exports should fail"); + ((PyByteArray)subject).bytearray_append(Py.One); // Appends one zero byte + fail("bytearray_append with exports should fail"); } catch (Exception e) { // Success } @@ -1509,19 +1509,6 @@ } @Override - public void releaseAction() { - // XXX Consider making this automatic within BaseBuffer.release() when getRoot()!=this - /* - * ... so that {@link #release()} takes care of this: sub-classes should not propagate - * the release themselves when overriding {@link #releaseAction()}. - */ - // We have to release the root too if ours was final and we are not that root. - if (root != this) { - root.release(); - } - } - - @Override public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { int newStart = index0 + start * strides[0]; int newStride = strides[0] * stride; -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:26 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:26 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Minor_additions_to_PyBuffer?= =?utf-8?q?_implementations_and_add_NEWS=2E?= Message-ID: <20160827131225.27675.35035.69AEF207@psf.io> https://hg.python.org/jython/rev/92b8a1f732f1 changeset: 7949:92b8a1f732f1 user: Jeff Allen date: Sat Aug 27 07:56:23 2016 +0100 summary: Minor additions to PyBuffer implementations and add NEWS. This change tidies up some comments and variable names, adds some bounds-checks. Finally, flag up the API change in NEWS. files: NEWS | 6 + src/org/python/core/PyBuffer.java | 6 +- src/org/python/core/buffer/BaseNIOBuffer.java | 75 ++++++--- src/org/python/core/buffer/SimpleNIOBuffer.java | 6 - tests/java/org/python/core/PyBufferTestSupport.java | 1 + 5 files changed, 56 insertions(+), 38 deletions(-) diff --git a/NEWS b/NEWS --- a/NEWS +++ b/NEWS @@ -9,6 +9,12 @@ New Features - Use latest upstream bundled wheels for ensurepip: pip (7.1.2), setuptools (18.4), replacing wheels patched specifically for Jython + - Buffer API changes allow java.nio.ByteBuffer to provide the storage when a PyBuffer + is exported. This is to support CPython extensions via JyNI, but has other uses too + (including access to direct memory buffers from Python). There is no change at the + Python level or for client code using PyBuffer via the "fully encapsulated" API. It + risks breaking code that makes direct access to a byte array via PyBuffer, implements + the PyBuffer interface, or extends implementation classes in org.python.core.buffer. Jython 2.7.1b2 Bugs fixed 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 @@ -151,9 +151,9 @@ throws IndexOutOfBoundsException, PyException; /** - * Copy the whole of another PyBuffer into this buffer. This may validly be done only for - * buffers that are consistent in their dimensions. When it is necessary to copy partial - * buffers, this may be achieved using a buffer slice on the source or destination. + * Copy the whole of another PyBuffer into this buffer. This may validly be done + * only for buffers that are consistent in their dimensions. When it is necessary to copy + * partial buffers, this may be achieved using a buffer slice on the source or destination. * * @param src source buffer * @throws IndexOutOfBoundsException if access out of bounds in source or destination diff --git a/src/org/python/core/buffer/BaseNIOBuffer.java b/src/org/python/core/buffer/BaseNIOBuffer.java --- a/src/org/python/core/buffer/BaseNIOBuffer.java +++ b/src/org/python/core/buffer/BaseNIOBuffer.java @@ -68,11 +68,12 @@ } @Override - protected void storeAtImpl(byte value, int byteIndex) throws IndexOutOfBoundsException, - PyException { - // XXX consider catching ReadonlyBufferException instead of checking (and others: index?) - checkWritable(); - storage.put(byteIndex, value); + protected void storeAtImpl(byte value, int byteIndex) throws PyException { + try { + storage.put(byteIndex, value); + } catch (ReadOnlyBufferException rbe) { + throw notWritable(); + } } @Override @@ -97,27 +98,33 @@ } /** - * {@inheritDoc} - *

- * The default implementation in BaseBuffer deals with the general one-dimensional - * case of arbitrary item size and stride. + * Copy all items in this buffer into a ByteBuffer, starting at its current + * position. + * + * @param dest destination buffer + * @throws BufferOverflowException + * @throws ReadOnlyBufferException */ // XXX Should this become part of the PyBUffer interface? - public void copyTo(ByteBuffer dest) throws BufferOverflowException, ReadOnlyBufferException, - PyException { + public void copyTo(ByteBuffer dest) throws BufferOverflowException, ReadOnlyBufferException { // Note shape[0] is the number of items in the buffer copyTo(0, dest, shape[0]); } /** - * {@inheritDoc} - *

- * The default implementation in BaseNIOBuffer deals with the general - * one-dimensional case of arbitrary item size and stride. + * Copy a specified number of items from a particular location in this buffer into a + * ByteBuffer, starting at its current position. . + * + * @param srcIndex index of the first item to copy + * @param dest destination buffer + * @param count number of items to copy + * @throws BufferOverflowException + * @throws ReadOnlyBufferException + * @throws IndexOutOfBoundsException */ // XXX Should this become part of the PyBuffer interface? protected void copyTo(int srcIndex, ByteBuffer dest, int count) throws BufferOverflowException, - ReadOnlyBufferException, IndexOutOfBoundsException, PyException { + ReadOnlyBufferException, IndexOutOfBoundsException { if (count > 0) { @@ -168,38 +175,48 @@ } /** - * {@inheritDoc} - *

- * The default implementation in BaseNIOBuffer deals with the general - * one-dimensional case of arbitrary item size and stride. + * Copy a specified number of items from a ByteBuffer into this buffer at a + * particular location. + * + * @param src source ByteBuffer + * @param destIndex starting item-index in the destination (i.e. this) + * @param count number of items to copy in + * @throws IndexOutOfBoundsException if access out of bounds in source or destination + * @throws PyException (TypeError) if read-only buffer */ // XXX Should this become part of the PyBUffer interface? - protected void copyFrom(ByteBuffer src, int dstIndex, int count) + protected void copyFrom(ByteBuffer src, int destIndex, int count) throws IndexOutOfBoundsException, PyException { checkWritable(); if (count > 0) { - ByteBuffer dst = getNIOByteBuffer(); - int pos = byteIndex(dstIndex); + ByteBuffer dest = getNIOByteBuffer(); + int pos = byteIndex(destIndex); // Pick up attributes necessary to choose an efficient copy strategy int itemsize = getItemsize(); int stride = getStrides()[0]; int skip = stride - itemsize; + int size = getSize(); + + // Check indexes in destination (this) using the "all non-negative" trick + if ((destIndex | count | size - (destIndex + count)) < 0) { + throw new IndexOutOfBoundsException(); + } // Strategy depends on whether items are laid end-to-end or there are gaps if (skip == 0) { // Straight copy of contiguous bytes - dst.position(pos); - dst.put(src); + dest.position(pos); + dest.put(src); } else if (itemsize == 1) { // Non-contiguous copy: single byte items for (int i = 0; i < count; i++) { - dst.position(pos); - dst.put(src.get()); + dest.position(pos); + dest.put(src.get()); // Next byte written will be here pos += stride; } @@ -207,10 +224,10 @@ } else { // Non-contiguous copy: each time, copy itemsize bytes at a time for (int i = 0; i < count; i++) { - dst.position(pos); + dest.position(pos); // Delineate the next itemsize bytes in the src src.limit(src.position() + itemsize); - dst.put(src); + dest.put(src); // Next byte written will be here pos += stride; } diff --git a/src/org/python/core/buffer/SimpleNIOBuffer.java b/src/org/python/core/buffer/SimpleNIOBuffer.java --- a/src/org/python/core/buffer/SimpleNIOBuffer.java +++ b/src/org/python/core/buffer/SimpleNIOBuffer.java @@ -9,15 +9,9 @@ /** * Buffer API over a read-only one-dimensional java.nio.ByteBuffer of one-byte items. */ - public class SimpleNIOBuffer extends BaseNIOBuffer { /** - * The strides array for this type is always a single element array with a 1 in it. - */ - protected static final int[] SIMPLE_STRIDES = {1}; // XXX Push up? - - /** * Provide an instance of SimpleNIOBuffer with navigation variables initialised, * for sub-class use. The buffer ({@link #storage}, {@link #index0}), and the {@link #shape} * array will be initialised from the arguments (which are checked for range). The diff --git a/tests/java/org/python/core/PyBufferTestSupport.java b/tests/java/org/python/core/PyBufferTestSupport.java --- a/tests/java/org/python/core/PyBufferTestSupport.java +++ b/tests/java/org/python/core/PyBufferTestSupport.java @@ -425,6 +425,7 @@ while (sb.length() < 30) { sb.append(' '); } + sb.append(view.isReadonly()?"R ":"W "); sb.append("ref = ").append(ref.toString()); return sb.toString(); -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:25 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:25 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Add_to_PyBuffer_a_reference?= =?utf-8?q?_to_the_exporting_object=2E?= Message-ID: <20160827131225.113279.72620.10ECBA63@psf.io> https://hg.python.org/jython/rev/59e3bae3c519 changeset: 7948:59e3bae3c519 user: Jeff Allen date: Mon Jul 11 13:33:02 2016 +0100 summary: Add to PyBuffer a reference to the exporting object. A getObj() method is added to the interface (emulating the CPython obj member), and is added to constructors for concrete implementations provided in the org.python.core.buffer package. files: src/org/python/core/PyArray.java | 9 +- src/org/python/core/PyBuffer.java | 8 ++ src/org/python/core/PyByteArray.java | 2 +- src/org/python/core/PyMemoryView.java | 17 +++- src/org/python/core/PyString.java | 2 +- src/org/python/core/buffer/BaseBuffer.java | 11 +++ src/org/python/core/buffer/BaseNIOBuffer.java | 2 - src/org/python/core/buffer/SimpleBuffer.java | 29 +++++--- src/org/python/core/buffer/SimpleNIOBuffer.java | 29 +++++--- src/org/python/core/buffer/SimpleStringBuffer.java | 10 +- src/org/python/core/buffer/SimpleWritableBuffer.java | 16 ++- src/org/python/core/buffer/Strided1DBuffer.java | 12 ++- src/org/python/core/buffer/Strided1DNIOBuffer.java | 17 +++- src/org/python/core/buffer/Strided1DWritableBuffer.java | 11 ++- src/org/python/core/buffer/ZeroByteBuffer.java | 8 +- src/org/python/modules/_io/PyIOBase.java | 9 +- tests/java/org/python/core/BaseBytesTest.java | 7 +- tests/java/org/python/core/PyBufferNIOTest.java | 22 +++-- tests/java/org/python/core/PyBufferTest.java | 34 ++++++--- 19 files changed, 163 insertions(+), 92 deletions(-) diff --git a/src/org/python/core/PyArray.java b/src/org/python/core/PyArray.java --- a/src/org/python/core/PyArray.java +++ b/src/org/python/core/PyArray.java @@ -2081,10 +2081,10 @@ // This is byte data, so we are within the state of the art byte[] storage = (byte[])data; int size = delegate.getSize(); - pybuf = new SimpleWritableBuffer(flags, storage, 0, size); + pybuf = new SimpleWritableBuffer(flags, this, storage, 0, size); } else if ((flags & PyBUF.WRITABLE) == 0) { // As the client only intends to read, fake the answer with a String - pybuf = new SimpleStringBuffer(flags, tostring()); + pybuf = new SimpleStringBuffer(flags, this, tostring()); } else { // For the time being ... throw Py.NotImplementedError("only array('b') can export a writable buffer"); @@ -2163,7 +2163,6 @@ return buf.remaining(); } - @Override public int read() { return buf.hasRemaining() ? buf.get() & 0xff : -1; @@ -2187,7 +2186,6 @@ } } - /* Traverseproc implementation */ @Override public int traverse(Visitproc visit, Object arg) { @@ -2198,8 +2196,7 @@ } @Override - public boolean refersDirectlyTo(PyObject ob) - throws UnsupportedOperationException { + public boolean refersDirectlyTo(PyObject ob) throws UnsupportedOperationException { if (data == null || !gc.canLinkToPyObject(data.getClass(), true)) { return false; } diff --git a/src/org/python/core/PyBuffer.java b/src/org/python/core/PyBuffer.java --- a/src/org/python/core/PyBuffer.java +++ b/src/org/python/core/PyBuffer.java @@ -30,6 +30,14 @@ // int getLen(); /** + * Return the underlying exporting object (or null if no object implementing the + * {@link BufferProtocol} is in that role). This will often be a PyObject. + * + * @return exporting object (or null) + */ + BufferProtocol getObj(); + + /** * Return the byte indexed from a one-dimensional buffer with item size one. This is part of the * fully-encapsulated API: the buffer implementation exported takes care of navigating the * structure of the buffer. Results are undefined where the number of dimensions is not one or diff --git a/src/org/python/core/PyByteArray.java b/src/org/python/core/PyByteArray.java --- a/src/org/python/core/PyByteArray.java +++ b/src/org/python/core/PyByteArray.java @@ -228,7 +228,7 @@ if (pybuf == null) { // No existing export we can re-use: create a new one - pybuf = new SimpleWritableBuffer(flags, storage, offset, size); + pybuf = new SimpleWritableBuffer(flags, this, storage, offset, size); // Hold a reference for possible re-use export = new WeakReference(pybuf); } diff --git a/src/org/python/core/PyMemoryView.java b/src/org/python/core/PyMemoryView.java --- a/src/org/python/core/PyMemoryView.java +++ b/src/org/python/core/PyMemoryView.java @@ -75,6 +75,13 @@ } } + // @ExposedGet(doc = obj_doc) // Not exposed in Python 2.7 + public PyObject obj() { + checkNotReleased(); + BufferProtocol obj = backing.getObj(); + return (obj instanceof PyObject) ? (PyObject)obj : Py.None; + } + @ExposedGet(doc = format_doc) public String format() { checkNotReleased(); @@ -843,19 +850,18 @@ } } - /* Traverseproc implementation */ @Override public int traverse(Visitproc visit, Object arg) { int retVal; if (backing != null) { if (backing instanceof PyObject) { - retVal = visit.visit((PyObject) backing, arg); + retVal = visit.visit((PyObject)backing, arg); if (retVal != 0) { return retVal; } } else if (backing instanceof Traverseproc) { - retVal = ((Traverseproc) backing).traverse(visit, arg); + retVal = ((Traverseproc)backing).traverse(visit, arg); if (retVal != 0) { return retVal; } @@ -878,11 +884,10 @@ @Override public boolean refersDirectlyTo(PyObject ob) { - if (ob != null && (ob == backing || ob == shape || ob == strides - || ob == suboffsets)) { + if (ob != null && (ob == backing || ob == shape || ob == strides || ob == suboffsets)) { return true; } else if (suboffsets instanceof Traverseproc) { - return ((Traverseproc) suboffsets).refersDirectlyTo(ob); + return ((Traverseproc)suboffsets).refersDirectlyTo(ob); } else { return false; } diff --git a/src/org/python/core/PyString.java b/src/org/python/core/PyString.java --- a/src/org/python/core/PyString.java +++ b/src/org/python/core/PyString.java @@ -204,7 +204,7 @@ * No existing export we can re-use. Return a buffer, but specialised to defer * construction of the buf object, and cache a soft reference to it. */ - pybuf = new SimpleStringBuffer(flags, getString()); + pybuf = new SimpleStringBuffer(flags, this, getString()); export = new SoftReference(pybuf); } return pybuf; 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 @@ -45,6 +45,12 @@ public abstract class BaseBuffer implements PyBuffer { /** + * The object that exported this buffer (or null if the subclass or exporter + * chooses not to supply a reference). + */ + protected BufferProtocol obj; + + /** * The dimensions of the array represented by the buffer. The length of the shape * array is the number of dimensions. The shape array should always be created and * filled (difference from CPython). This value is returned by {@link #getShape()}. @@ -255,6 +261,11 @@ return len; } + @Override + public final BufferProtocol getObj() { + return obj; + } + /** * Retrieve the byte at the given index in the underlying storage treated as a flat sequence of * bytes. This byte-index will have been computed from the item index (which may have been diff --git a/src/org/python/core/buffer/BaseNIOBuffer.java b/src/org/python/core/buffer/BaseNIOBuffer.java --- a/src/org/python/core/buffer/BaseNIOBuffer.java +++ b/src/org/python/core/buffer/BaseNIOBuffer.java @@ -5,7 +5,6 @@ import java.nio.ReadOnlyBufferException; import org.python.core.PyBUF; -import org.python.core.PyBuffer; import org.python.core.PyException; /** @@ -50,7 +49,6 @@ * @param size number of elements in the view * @param stride byte-index step between successive elements */ - protected BaseNIOBuffer(ByteBuffer storage, int featureFlags, int index0, int size, int stride) { super(featureFlags & ~(WRITABLE | AS_ARRAY), index0, size, stride); this.storage = storage; diff --git a/src/org/python/core/buffer/SimpleBuffer.java b/src/org/python/core/buffer/SimpleBuffer.java --- a/src/org/python/core/buffer/SimpleBuffer.java +++ b/src/org/python/core/buffer/SimpleBuffer.java @@ -1,5 +1,6 @@ package org.python.core.buffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.PyException; import org.python.core.util.StringUtil; @@ -22,14 +23,16 @@ * checkRequestFlags(flags); // Check request is compatible with type *

* + * @param obj exporting object (or null) * @param storage the array of bytes storing the implementation of the exporting object * @param index0 offset where the data starts in that array (item[0]) * @param size the number of bytes occupied * @throws NullPointerException if storage is null */ - protected SimpleBuffer(byte[] storage, int index0, int size) throws PyException, - ArrayIndexOutOfBoundsException { + protected SimpleBuffer(BufferProtocol obj, byte[] storage, int index0, int size) + throws PyException, ArrayIndexOutOfBoundsException { super(storage, CONTIGUITY | SIMPLE, index0, size, 1); + this.obj = obj; } /** @@ -38,6 +41,7 @@ * against the capabilities of the buffer type. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage the array of bytes storing the implementation of the exporting object * @param index0 offset where the data starts in that array (item[0]) * @param size the number of bytes occupied @@ -46,10 +50,10 @@ * inconsistent with storage.length * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SimpleBuffer(int flags, byte[] storage, int index0, int size) throws PyException, - ArrayIndexOutOfBoundsException, NullPointerException { - this(storage, index0, size); // Construct checked SimpleBuffer - checkRequestFlags(flags); // Check request is compatible with type + public SimpleBuffer(int flags, BufferProtocol obj, byte[] storage, int index0, int size) + throws PyException, ArrayIndexOutOfBoundsException, NullPointerException { + this(obj, storage, index0, size); // Construct checked SimpleBuffer + checkRequestFlags(flags); // Check request is compatible with type // Check arguments using the "all non-negative" trick if ((index0 | size | storage.length - (index0 + size)) < 0) { throw new ArrayIndexOutOfBoundsException(); @@ -62,11 +66,12 @@ * {@link #index0}), and the navigation ({@link #shape} array) will be initialised from the * array argument. * + * @param obj exporting object (or null) * @param storage the array of bytes storing the implementation of the exporting object * @throws NullPointerException if storage is null */ - protected SimpleBuffer(byte[] storage) throws NullPointerException { - this(storage, 0, storage.length); + protected SimpleBuffer(BufferProtocol obj, byte[] storage) throws NullPointerException { + this(obj, storage, 0, storage.length); } /** @@ -75,12 +80,14 @@ * against the capabilities of the buffer type. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage the array of bytes storing the implementation of the exporting object * @throws NullPointerException if storage is null * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SimpleBuffer(int flags, byte[] storage) throws PyException, NullPointerException { - this(storage); // Construct SimpleBuffer on whole array + public SimpleBuffer(int flags, BufferProtocol obj, byte[] storage) throws PyException, + NullPointerException { + this(obj, storage); // Construct SimpleBuffer on whole array checkRequestFlags(flags); // Check request is compatible with type } @@ -189,7 +196,7 @@ */ public SimpleView(PyBuffer root, int flags, byte[] storage, int offset, int size) { // Create a new SimpleBuffer on the buffer passed in (part of the root) - super(flags, storage, offset, size); + super(flags, root.getObj(), storage, offset, size); // Get a lease on the root PyBuffer this.root = root.getBuffer(FULL_RO); } diff --git a/src/org/python/core/buffer/SimpleNIOBuffer.java b/src/org/python/core/buffer/SimpleNIOBuffer.java --- a/src/org/python/core/buffer/SimpleNIOBuffer.java +++ b/src/org/python/core/buffer/SimpleNIOBuffer.java @@ -2,6 +2,7 @@ import java.nio.ByteBuffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.PyException; @@ -29,6 +30,7 @@ * checkRequestFlags(flags); // Check request is compatible with type *
* + * @param obj exporting object (or null) * @param storage the ByteBuffer wrapping the exported object state. NOTE: this * PyBuffer keeps a reference and may manipulate the position, mark and * limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy. @@ -38,9 +40,10 @@ * @throws ArrayIndexOutOfBoundsException if index0 and size are * inconsistent with storage.capacity() */ - protected SimpleNIOBuffer(ByteBuffer storage, int index0, int size) throws PyException, - ArrayIndexOutOfBoundsException { + protected SimpleNIOBuffer(BufferProtocol obj, ByteBuffer storage, int index0, int size) + throws PyException, ArrayIndexOutOfBoundsException { super(storage, CONTIGUITY | SIMPLE, index0, size, 1); + this.obj = obj; // Check arguments using the "all non-negative" trick if ((index0 | size | storage.capacity() - (index0 + size)) < 0) { throw new ArrayIndexOutOfBoundsException(); @@ -54,6 +57,7 @@ * ByteBuffer passed in. (It is duplicated.) * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage the ByteBuffer wrapping the exported object state * @param index0 offset where the data starts in that buffer (item[0]) * @param size the number of bytes occupied @@ -62,10 +66,10 @@ * inconsistent with storage.length * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SimpleNIOBuffer(int flags, ByteBuffer storage, int index0, int size) throws PyException, - ArrayIndexOutOfBoundsException, NullPointerException { - this(storage.duplicate(), index0, size); // Construct checked SimpleNIOBuffer - checkRequestFlags(flags); // Check request is compatible with type + public SimpleNIOBuffer(int flags, BufferProtocol obj, ByteBuffer storage, int index0, int size) + throws PyException, ArrayIndexOutOfBoundsException, NullPointerException { + this(obj, storage.duplicate(), index0, size); // Construct checked SimpleNIOBuffer + checkRequestFlags(flags); // Check request is compatible with type } /** @@ -74,13 +78,14 @@ * {@link #index0}), and the navigation ({@link #shape} array) will be initialised from the * argument. * + * @param obj exporting object (or null) * @param storage the ByteBuffer wrapping the exported object state. NOTE: this * PyBuffer keeps a reference and may manipulate the position, mark and * limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy. * @throws NullPointerException if storage is null */ - protected SimpleNIOBuffer(ByteBuffer storage) throws NullPointerException { - this(storage, 0, storage.capacity()); + protected SimpleNIOBuffer(BufferProtocol obj, ByteBuffer storage) throws NullPointerException { + this(obj, storage, 0, storage.capacity()); } /** @@ -90,12 +95,14 @@ * ByteBuffer passed in. (It is duplicated.) * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage the ByteBuffer wrapping the exported object state * @throws NullPointerException if storage is null * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SimpleNIOBuffer(int flags, ByteBuffer storage) throws PyException, NullPointerException { - this(storage.duplicate()); // Construct SimpleNIOBuffer on whole ByteBuffer + public SimpleNIOBuffer(int flags, BufferProtocol obj, ByteBuffer storage) throws PyException, + NullPointerException { + this(obj, storage.duplicate()); // Construct SimpleNIOBuffer on whole ByteBuffer checkRequestFlags(flags); // Check request is compatible with type } @@ -176,7 +183,7 @@ */ public SimpleView(PyBuffer root, int flags, ByteBuffer storage, int offset, int count) { // Create a new SimpleNIOBuffer on the buffer passed in (part of the root) - super(flags, storage, offset, count); + super(flags, root.getObj(), storage, offset, count); // Get a lease on the root PyBuffer this.root = root.getBuffer(FULL_RO); } diff --git a/src/org/python/core/buffer/SimpleStringBuffer.java b/src/org/python/core/buffer/SimpleStringBuffer.java --- a/src/org/python/core/buffer/SimpleStringBuffer.java +++ b/src/org/python/core/buffer/SimpleStringBuffer.java @@ -2,6 +2,7 @@ import java.nio.ByteBuffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.util.StringUtil; @@ -26,15 +27,16 @@ * Provide an instance of SimpleStringBuffer meeting the consumer's expectations as expressed in * the flags argument. * + * @param flags consumer requirements + * @param obj exporting object (or null) * @param bufString storing the implementation of the object - * @param flags consumer requirements */ - public SimpleStringBuffer(int flags, String bufString) { + public SimpleStringBuffer(int flags, BufferProtocol obj, String bufString) { /* * Leaving storage=null is ok because we carefully override every method that uses it, * deferring creation of the storage byte array until we absolutely must have one. */ - super(null, 0, bufString.length()); + super(obj, null, 0, bufString.length()); // Save the backing string this.bufString = bufString; // Check request is compatible with type @@ -205,7 +207,7 @@ */ public SimpleStringView(PyBuffer root, int flags, String bufString) { // Create a new SimpleStringBuffer on the string passed in - super(flags, bufString); + super(flags, root.getObj(), bufString); // Get a lease on the root PyBuffer this.root = root.getBuffer(FULL_RO); } diff --git a/src/org/python/core/buffer/SimpleWritableBuffer.java b/src/org/python/core/buffer/SimpleWritableBuffer.java --- a/src/org/python/core/buffer/SimpleWritableBuffer.java +++ b/src/org/python/core/buffer/SimpleWritableBuffer.java @@ -1,5 +1,6 @@ package org.python.core.buffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.PyException; @@ -14,16 +15,17 @@ * against the capabilities of the buffer type. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage the array of bytes storing the implementation of the exporting object * @param index0 offset where the data starts in that array (item[0]) * @param size the number of bytes occupied * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SimpleWritableBuffer(int flags, byte[] storage, int index0, int size) + public SimpleWritableBuffer(int flags, BufferProtocol obj, byte[] storage, int index0, int size) throws PyException, NullPointerException { - super(storage, index0, size); // Construct checked SimpleBuffer + super(obj, storage, index0, size); // Construct checked SimpleBuffer addFeatureFlags(WRITABLE); - checkRequestFlags(flags); // Check request is compatible with type + checkRequestFlags(flags); // Check request is compatible with type } /** @@ -32,11 +34,13 @@ * checked against the capabilities of the buffer type. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage the array of bytes storing the implementation of the exporting object * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SimpleWritableBuffer(int flags, byte[] storage) throws PyException, NullPointerException { - this(flags, storage, 0, storage.length); + public SimpleWritableBuffer(int flags, BufferProtocol obj, byte[] storage) throws PyException, + NullPointerException { + this(flags, obj, storage, 0, storage.length); } /** @@ -121,7 +125,7 @@ */ public SimpleView(PyBuffer root, int flags, byte[] storage, int index0, int size) { // Create a new SimpleBuffer on the buffer passed in (part of the root) - super(flags, storage, index0, size); + super(flags, root.getObj(), storage, index0, size); // Get a lease on the root PyBuffer this.root = root.getBuffer(FULL_RO); } diff --git a/src/org/python/core/buffer/Strided1DBuffer.java b/src/org/python/core/buffer/Strided1DBuffer.java --- a/src/org/python/core/buffer/Strided1DBuffer.java +++ b/src/org/python/core/buffer/Strided1DBuffer.java @@ -1,5 +1,6 @@ package org.python.core.buffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.PyException; @@ -48,6 +49,7 @@ * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, byte[], int, int, int)} for an * example of this use.) * + * @param obj exporting object (or null) * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] * @param count number of items in the slice @@ -56,9 +58,10 @@ * @throws ArrayIndexOutOfBoundsException if index0, count and * stride are inconsistent with storage.length */ - protected Strided1DBuffer(byte[] storage, int index0, int count, int stride) + protected Strided1DBuffer(BufferProtocol obj, byte[] storage, int index0, int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException { super(storage, STRIDES, index0, count, stride); + this.obj = obj; this.stride = stride; // Between items if (count == 0) { @@ -107,6 +110,7 @@ * storage array (unless count=0). * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] * @param count number of items in the slice @@ -116,9 +120,9 @@ * stride are inconsistent with storage.length * @throws PyException (BufferError) when expectations do not correspond with the type */ - public Strided1DBuffer(int flags, byte[] storage, int index0, int count, int stride) + public Strided1DBuffer(int flags, BufferProtocol obj, byte[] storage, int index0, int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException, PyException { - this(storage, index0, count, stride); + this(obj, storage, index0, count, stride); checkRequestFlags(flags); // Check request is compatible with type } @@ -199,7 +203,7 @@ public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int count, int stride) throws PyException { // Create a new on the buffer passed in (part of the root) - super(flags, storage, index0, count, stride); + super(flags, root.getObj(), storage, index0, count, stride); // Get a lease on the root PyBuffer (read-only) this.root = root.getBuffer(FULL_RO); } diff --git a/src/org/python/core/buffer/Strided1DNIOBuffer.java b/src/org/python/core/buffer/Strided1DNIOBuffer.java --- a/src/org/python/core/buffer/Strided1DNIOBuffer.java +++ b/src/org/python/core/buffer/Strided1DNIOBuffer.java @@ -2,6 +2,7 @@ import java.nio.ByteBuffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.PyException; @@ -50,6 +51,7 @@ * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, ByteBuffer, int, int, int)} for * an example of this use.) * + * @param obj exporting object (or null) * @param storage the ByteBuffer wrapping the exported object state. NOTE: this * PyBuffer keeps a reference and may manipulate the position, mark and * limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy. @@ -60,9 +62,10 @@ * @throws ArrayIndexOutOfBoundsException if index0, count and * stride are inconsistent with storage.length */ - protected Strided1DNIOBuffer(ByteBuffer storage, int index0, int count, int stride) - throws ArrayIndexOutOfBoundsException, NullPointerException { + protected Strided1DNIOBuffer(BufferProtocol obj, ByteBuffer storage, int index0, int count, + int stride) throws ArrayIndexOutOfBoundsException, NullPointerException { super(storage, STRIDES, index0, count, stride); + this.obj = obj; this.stride = stride; // Between items if (count == 0) { @@ -121,6 +124,7 @@ * storage array (unless count=0). * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage ByteBuffer wrapping exported data * @param index0 index into storage of item[0] * @param count number of items in the slice @@ -130,9 +134,10 @@ * stride are inconsistent with storage.length * @throws PyException (BufferError) when expectations do not correspond with the type */ - public Strided1DNIOBuffer(int flags, ByteBuffer storage, int index0, int count, int stride) - throws ArrayIndexOutOfBoundsException, NullPointerException, PyException { - this(storage.duplicate(), index0, count, stride); + public Strided1DNIOBuffer(int flags, BufferProtocol obj, ByteBuffer storage, int index0, + int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException, + PyException { + this(obj, storage.duplicate(), index0, count, stride); checkRequestFlags(flags); // Check request is compatible with type } @@ -191,7 +196,7 @@ public SlicedView(PyBuffer root, int flags, ByteBuffer storage, int index0, int count, int stride) throws PyException { // Create a new slice on the buffer passed in (part of the root) - super(flags, storage, index0, count, stride); + super(flags, root.getObj(), storage, index0, count, stride); // Get a lease on the root PyBuffer (read-only) this.root = root.getBuffer(FULL_RO); } diff --git a/src/org/python/core/buffer/Strided1DWritableBuffer.java b/src/org/python/core/buffer/Strided1DWritableBuffer.java --- a/src/org/python/core/buffer/Strided1DWritableBuffer.java +++ b/src/org/python/core/buffer/Strided1DWritableBuffer.java @@ -1,5 +1,6 @@ package org.python.core.buffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.PyException; @@ -25,6 +26,7 @@ * the sub-range the caller is allowed to use. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] * @param count number of items in the slice @@ -34,9 +36,10 @@ * stride are inconsistent with storage.length * @throws PyException (BufferError) when expectations do not correspond with the type */ - public Strided1DWritableBuffer(int flags, byte[] storage, int index0, int count, int stride) - throws ArrayIndexOutOfBoundsException, NullPointerException, PyException { - super(storage, index0, count, stride); + public Strided1DWritableBuffer(int flags, BufferProtocol obj, byte[] storage, int index0, + int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException, + PyException { + super(obj, storage, index0, count, stride); addFeatureFlags(WRITABLE); checkRequestFlags(flags); // Check request is compatible with type } @@ -107,7 +110,7 @@ public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int count, int stride) throws PyException { // Create a new on the buffer passed in (part of the root) - super(flags, storage, index0, count, stride); + super(flags, root.getObj(), storage, index0, count, stride); // Get a lease on the root PyBuffer (writable) this.root = root.getBuffer(FULL); } diff --git a/src/org/python/core/buffer/ZeroByteBuffer.java b/src/org/python/core/buffer/ZeroByteBuffer.java --- a/src/org/python/core/buffer/ZeroByteBuffer.java +++ b/src/org/python/core/buffer/ZeroByteBuffer.java @@ -1,5 +1,6 @@ package org.python.core.buffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.PyException; @@ -25,12 +26,15 @@ * client code that may ask, if the results are customary for the exporting object. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param readonly set true if not to be considered writable * @param hasArray set true if to be considered as backed by an array * @throws PyException (BufferError) when client expectations do not correspond with the type */ - public ZeroByteBuffer(int flags, boolean readonly, boolean hasArray) throws PyException { + public ZeroByteBuffer(int flags, BufferProtocol obj, boolean readonly, boolean hasArray) + throws PyException { super(EMPTY, CONTIGUITY | (readonly ? 0 : WRITABLE), 0, 0, 1); + this.obj = obj; if (!hasArray) { // super() knows we have an array, but this truth is inconvenient here. removeFeatureFlags(AS_ARRAY); @@ -175,7 +179,7 @@ */ public View(PyBuffer root, int flags) { // Create a new ZeroByteBuffer on who-cares-what byte array - super(flags, root.isReadonly(), root.hasArray()); + super(flags, root.getObj(), root.isReadonly(), root.hasArray()); // But we still have to get a lease on the root PyBuffer this.root = root.getBuffer(FULL_RO); } diff --git a/src/org/python/modules/_io/PyIOBase.java b/src/org/python/modules/_io/PyIOBase.java --- a/src/org/python/modules/_io/PyIOBase.java +++ b/src/org/python/modules/_io/PyIOBase.java @@ -778,7 +778,7 @@ // None of the above: complain throw tailoredTypeError("read-write buffer", obj); } - return new SimpleStringBuffer(PyBUF.SIMPLE, s); + return new SimpleStringBuffer(PyBUF.SIMPLE, null, s); } } @@ -910,18 +910,17 @@ + "fp is closed after the suite of the with statement is complete:\n" + "\n" + "with open('spam.txt', 'r') as fp:\n" + " fp.write('Spam and eggs!')\n"; - /* Traverseproc implementation */ @Override public int traverse(Visitproc visit, Object arg) { - //closer cannot be null + // closer cannot be null if (closer.sys != null) { - int retVal = visit.visit(closer.sys, arg); + int retVal = visit.visit(closer.sys, arg); if (retVal != 0) { return retVal; } } - //__dict__ cannot be null + // __dict__ cannot be null return visit.visit(__dict__, arg); } diff --git a/tests/java/org/python/core/BaseBytesTest.java b/tests/java/org/python/core/BaseBytesTest.java --- a/tests/java/org/python/core/BaseBytesTest.java +++ b/tests/java/org/python/core/BaseBytesTest.java @@ -203,6 +203,7 @@ * * @see junit.framework.TestCase#setUp() */ + @Override protected void setUp() throws Exception { super.setUp(); random = new Random(20120310L); @@ -595,7 +596,7 @@ */ public MyBytes(BufferProtocol value) { super(TYPE); - init((BufferProtocol)value.getBuffer(PyBUF.SIMPLE)); + init(value.getBuffer(PyBUF.SIMPLE)); } /** @@ -787,7 +788,7 @@ @Override public PyBuffer getBuffer(int flags) { - return new SimpleBuffer(flags, store); + return new SimpleBuffer(flags, this, store); } } @@ -824,7 +825,7 @@ return; } for (int i = 0; i < n; i++) { - int c = 0xff & ((int)s[pos + i]); + int c = 0xff & (s[pos + i]); if (c == 0) { c = '.'; } else if (Character.isISOControl(c)) { diff --git a/tests/java/org/python/core/PyBufferNIOTest.java b/tests/java/org/python/core/PyBufferNIOTest.java --- a/tests/java/org/python/core/PyBufferNIOTest.java +++ b/tests/java/org/python/core/PyBufferNIOTest.java @@ -119,7 +119,7 @@ BaseBuffer pybuf = getExistingBuffer(flags); if (pybuf == null) { // No existing export we can re-use - pybuf = new SimpleNIOBuffer(flags, storage) { + pybuf = new SimpleNIOBuffer(flags, this, storage) { @Override protected void releaseAction() { @@ -200,7 +200,7 @@ BaseBuffer pybuf = getExistingBuffer(flags); if (pybuf == null) { // No existing export we can re-use - pybuf = new RollYourOwnNIOBuffer(flags, storage); + pybuf = new RollYourOwnNIOBuffer(flags, this, storage); // Hold a reference for possible re-use export = new WeakReference(pybuf); } @@ -227,26 +227,28 @@ * contiguous sequence of bytes from the position to the limit. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage buffer exported (from the position to the limit) */ - public RollYourOwnNIOBuffer(int flags, ByteBuffer storage) { - this(null /* =this */, flags, storage, storage.position(), storage.remaining(), 1); + public RollYourOwnNIOBuffer(int flags, BufferProtocol obj, ByteBuffer storage) { + this(flags, null /* =this */, obj, storage, storage.position(), storage.remaining(), 1); } /** * Construct a slice of a one-dimensional byte buffer. * + * @param flags consumer requirements + * @param obj exporting object (or null) * @param root on which release must be called when this is released - * @param flags consumer requirements * @param storage buffer containing exported data * @param index0 index into storage of item[0] * @param count number of items in the slice * @param stride in between successive elements of the new PyBuffer * @throws PyException (BufferError) when expectations do not correspond with the type */ - public RollYourOwnNIOBuffer(PyBuffer root, int flags, ByteBuffer storage, int index0, - int count, int stride) throws IndexOutOfBoundsException, NullPointerException, - PyException { + public RollYourOwnNIOBuffer(int flags, PyBuffer root, BufferProtocol obj, + ByteBuffer storage, int index0, int count, int stride) + throws IndexOutOfBoundsException, NullPointerException, PyException { // Client will need to navigate using shape and strides if this is a slice super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : STRIDES), // index0, new int[] {count}, new int[] {stride}); @@ -265,8 +267,10 @@ // Get a lease on the root PyBuffer (read-only). Last in case a check above fails. if (root == null) { this.root = this; + this.obj = obj; } else { this.root = root.getBuffer(FULL_RO); + this.obj = root.getObj(); } } @@ -279,7 +283,7 @@ public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { int newStart = index0 + start * strides[0]; int newStride = strides[0] * stride; - return new RollYourOwnNIOBuffer(root, flags, storage, newStart, count, newStride); + return new RollYourOwnNIOBuffer(flags, root, null, storage, newStart, count, newStride); } @Override 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 @@ -273,6 +273,13 @@ assertEquals("unexpected length", ref.length, view.getLen()); } + /** Test method for {@link org.python.core.PyBUF#getObj()}. */ + @Test + public void testGetObj() { + announce("getObj"); + assertEquals("unexpected exporting object", obj, view.getObj()); + } + /** Test method for {@link org.python.core.PyBuffer#byteAt(int)}. */ @Test public void testByteAt() { @@ -1279,7 +1286,7 @@ @Override public PyBuffer getBuffer(int flags) { - return new SimpleBuffer(flags, storage); + return new SimpleBuffer(flags, this, storage); } } @@ -1368,7 +1375,7 @@ BaseBuffer pybuf = getExistingBuffer(flags); if (pybuf == null) { // No existing export we can re-use - pybuf = new SimpleStringBuffer(flags, storage); + pybuf = new SimpleStringBuffer(flags, this, storage); // Hold a reference for possible re-use export = new SoftReference(pybuf); } @@ -1404,7 +1411,7 @@ BaseBuffer pybuf = getExistingBuffer(flags); if (pybuf == null) { // No existing export we can re-use - pybuf = new SimpleWritableBuffer(flags, storage) { + pybuf = new SimpleWritableBuffer(flags, this, storage) { @Override protected void releaseAction() { @@ -1435,7 +1442,7 @@ BaseBuffer pybuf = getExistingBuffer(flags); if (pybuf == null) { // No existing export we can re-use - pybuf = new RollYourOwnArrayBuffer(flags, storage); + pybuf = new RollYourOwnArrayBuffer(flags, this, storage); // Hold a reference for possible re-use export = new WeakReference(pybuf); } @@ -1461,26 +1468,28 @@ * Create a buffer view of the entire array. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage byte array exported in its entirety */ - public RollYourOwnArrayBuffer(int flags, byte[] storage) { - this(null /* =this */, flags, storage, 0, storage.length, 1); + public RollYourOwnArrayBuffer(int flags, BufferProtocol obj, byte[] storage) { + this(flags, null /* =this */, obj, storage, 0, storage.length, 1); } /** * Construct a slice of a one-dimensional byte array. * + * @param flags consumer requirements * @param root on which release must be called when this is released - * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] * @param count number of items in the slice * @param stride in between successive elements of the new PyBuffer * @throws PyException (BufferError) when expectations do not correspond with the type */ - public RollYourOwnArrayBuffer(PyBuffer root, int flags, byte[] storage, int index0, - int count, int stride) throws IndexOutOfBoundsException, NullPointerException, - PyException { + public RollYourOwnArrayBuffer(int flags, PyBuffer root, BufferProtocol obj, byte[] storage, + int index0, int count, int stride) throws IndexOutOfBoundsException, + NullPointerException, PyException { // Client will need to navigate using shape and strides if this is a slice super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : STRIDES), // index0, new int[] {count}, new int[] {stride}); @@ -1498,8 +1507,10 @@ // Get a lease on the root PyBuffer (read-only). Last in case a check above fails. if (root == null) { this.root = this; + this.obj = obj; } else { this.root = root.getBuffer(FULL_RO); + this.obj = root.getObj(); } } @@ -1512,7 +1523,8 @@ public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { int newStart = index0 + start * strides[0]; int newStride = strides[0] * stride; - return new RollYourOwnArrayBuffer(root, flags, storage, newStart, length, newStride); + return new RollYourOwnArrayBuffer(flags, root, null, storage, newStart, length, + newStride); } @Override -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 09:12:26 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 13:12:26 +0000 Subject: [Jython-checkins] =?utf-8?q?jython_=28merge_default_-=3E_default?= =?utf-8?q?=29=3A_Merge_of_buffer_API_changes_to_trunk?= Message-ID: <20160827131226.24258.47231.B54E6447@psf.io> https://hg.python.org/jython/rev/c87db62a1b0e changeset: 7950:c87db62a1b0e parent: 7935:35fa19ca1859 parent: 7949:92b8a1f732f1 user: Jeff Allen date: Sat Aug 27 14:10:53 2016 +0100 summary: Merge of buffer API changes to trunk files: NEWS | 6 + build.xml | 4 +- src/org/python/core/PyArray.java | 9 +- src/org/python/core/PyBUF.java | 30 +- src/org/python/core/PyBuffer.java | 125 +- src/org/python/core/PyByteArray.java | 2 +- src/org/python/core/PyMemoryView.java | 17 +- src/org/python/core/PyString.java | 2 +- src/org/python/core/buffer/Base1DBuffer.java | 117 + src/org/python/core/buffer/BaseArrayBuffer.java | 228 + src/org/python/core/buffer/BaseBuffer.java | 647 +- src/org/python/core/buffer/BaseNIOBuffer.java | 249 + src/org/python/core/buffer/SimpleBuffer.java | 180 +- src/org/python/core/buffer/SimpleNIOBuffer.java | 190 + src/org/python/core/buffer/SimpleStringBuffer.java | 87 +- src/org/python/core/buffer/SimpleWritableBuffer.java | 94 +- src/org/python/core/buffer/Strided1DBuffer.java | 157 +- src/org/python/core/buffer/Strided1DNIOBuffer.java | 209 + src/org/python/core/buffer/Strided1DWritableBuffer.java | 82 +- src/org/python/core/buffer/ZeroByteBuffer.java | 75 +- src/org/python/modules/_io/PyIOBase.java | 9 +- tests/java/org/python/core/BaseBytesTest.java | 7 +- tests/java/org/python/core/ByteBufferTestSupport.java | 585 ++ tests/java/org/python/core/PyBufferNIOTest.java | 305 + tests/java/org/python/core/PyBufferTest.java | 2419 ++++----- tests/java/org/python/core/PyBufferTestSupport.java | 544 ++ 26 files changed, 4276 insertions(+), 2103 deletions(-) diff --git a/NEWS b/NEWS --- a/NEWS +++ b/NEWS @@ -22,6 +22,12 @@ - Added uname function to posix module. The mostly Java-based implementation even works to some extend on non-posix systems. Additional tweaks extend this to full featured uname-functionality on Windows systems (usable via os.uname()). + - Buffer API changes allow java.nio.ByteBuffer to provide the storage when a PyBuffer + is exported. This is to support CPython extensions via JyNI, but has other uses too + (including access to direct memory buffers from Python). There is no change at the + Python level or for client code using PyBuffer via the "fully encapsulated" API. It + risks breaking code that makes direct access to a byte array via PyBuffer, implements + the PyBuffer interface, or extends implementation classes in org.python.core.buffer. Jython 2.7.1b3 Bugs fixed diff --git a/build.xml b/build.xml --- a/build.xml +++ b/build.xml @@ -928,7 +928,7 @@ - + @@ -950,7 +950,7 @@ - + diff --git a/src/org/python/core/PyArray.java b/src/org/python/core/PyArray.java --- a/src/org/python/core/PyArray.java +++ b/src/org/python/core/PyArray.java @@ -2081,10 +2081,10 @@ // This is byte data, so we are within the state of the art byte[] storage = (byte[])data; int size = delegate.getSize(); - pybuf = new SimpleWritableBuffer(flags, storage, 0, size); + pybuf = new SimpleWritableBuffer(flags, this, storage, 0, size); } else if ((flags & PyBUF.WRITABLE) == 0) { // As the client only intends to read, fake the answer with a String - pybuf = new SimpleStringBuffer(flags, tostring()); + pybuf = new SimpleStringBuffer(flags, this, tostring()); } else { // For the time being ... throw Py.NotImplementedError("only array('b') can export a writable buffer"); @@ -2163,7 +2163,6 @@ return buf.remaining(); } - @Override public int read() { return buf.hasRemaining() ? buf.get() & 0xff : -1; @@ -2187,7 +2186,6 @@ } } - /* Traverseproc implementation */ @Override public int traverse(Visitproc visit, Object arg) { @@ -2198,8 +2196,7 @@ } @Override - public boolean refersDirectlyTo(PyObject ob) - throws UnsupportedOperationException { + public boolean refersDirectlyTo(PyObject ob) throws UnsupportedOperationException { if (data == null || !gc.canLinkToPyObject(data.getClass(), true)) { return false; } diff --git a/src/org/python/core/PyBUF.java b/src/org/python/core/PyBUF.java --- a/src/org/python/core/PyBUF.java +++ b/src/org/python/core/PyBUF.java @@ -51,17 +51,17 @@ int[] getShape(); /** - * The number of units (bytes) stored in each indexable item. + * The number of bytes stored in each indexable item. * - * @return the number of units (bytes) comprising each item. + * @return the number of bytes comprising each item. */ int getItemsize(); /** - * The total number of units (bytes) stored, which will be the product of the elements of the - * shape array, and the item size in units. + * The total number of bytes represented by the view, which will be the product of the elements of the + * shape array, and the item size in bytes. * - * @return the total number of units stored. + * @return the total number of bytes represented. */ int getLen(); @@ -122,7 +122,7 @@ /** * A constant used by the consumer in its call to {@link BufferProtocol#getBuffer(int)} to * specify that it requires {@link PyBuffer#getFormat()} to return a String - * indicating the type of the unit. This exists for compatibility with CPython, as Jython as the + * indicating the type of the unit. This exists for compatibility with CPython, as in Jython the * format is always provided by getFormat(). */ static final int FORMAT = 0x0004; @@ -143,7 +143,7 @@ static final int STRIDES = 0x0010 | ND; /** * A constant used by the consumer in its call to {@link BufferProtocol#getBuffer(int)} to - * specify that it will assume C-order organisation of the units. getBuffer will + * specify that it will assume C-order organisation of the items. getBuffer will * raise an exception if the exporter's buffer is not C-ordered. C_CONTIGUOUS * implies STRIDES. */ @@ -152,14 +152,14 @@ static final int C_CONTIGUOUS = 0x0020 | STRIDES; /** * A constant used by the consumer in its call to {@link BufferProtocol#getBuffer(int)} to - * specify that it will assume Fortran-order organisation of the units. getBuffer + * specify that it will assume Fortran-order organisation of the items. getBuffer * will raise an exception if the exporter's buffer is not Fortran-ordered. * F_CONTIGUOUS implies STRIDES. */ static final int F_CONTIGUOUS = 0x0040 | STRIDES; /** * A constant used by the consumer in its call to {@link BufferProtocol#getBuffer(int)} to - * specify that it will assume a contiguous organisation of the units, but will enquire which + * specify that it will assume a contiguous organisation of the items, but will enquire which * organisation it actually is. * * getBuffer will raise an exception if the exporter's buffer is not contiguous. @@ -203,13 +203,14 @@ * 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()}. + * {@link PyBuffer#getNIOByteBuffer()} or using {@link PyBuffer#Pointer()}. */ static final int FULL = INDIRECT | WRITABLE | FORMAT; /** * 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()}. + * only, without ever calling {@link PyBuffer#getNIOByteBuffer()} or using + * {@link PyBuffer#Pointer()}. */ static final int FULL_RO = INDIRECT | FORMAT; @@ -221,7 +222,8 @@ * through the purely abstract part of the API). getBuffer will raise an exception * if the exporter cannot expose its storage as Java array. */ - static final int AS_ARRAY = 0x10000000; + // XXX Pending: @Deprecated + static final int AS_ARRAY = 0x10000000; /* Constants for readability, not standard for CPython */ @@ -242,13 +244,13 @@ static final int NAVIGATION = SIMPLE | ND | STRIDES | INDIRECT; /** * A constant used by the exporter in processing {@link BufferProtocol#getBuffer(int)} to check - * for assumed C-order organisation of the units. + * for assumed C-order organisation of the items. * C_CONTIGUOUS = IS_C_CONTIGUOUS | STRIDES. */ static final int IS_C_CONTIGUOUS = C_CONTIGUOUS & ~STRIDES; /** * A constant used by the exporter in processing {@link BufferProtocol#getBuffer(int)} to check - * for assumed C-order Fortran-order organisation of the units. + * for assumed C-order Fortran-order organisation of the items. * F_CONTIGUOUS = IS_F_CONTIGUOUS | STRIDES. */ static final int IS_F_CONTIGUOUS = F_CONTIGUOUS & ~STRIDES; diff --git a/src/org/python/core/PyBuffer.java b/src/org/python/core/PyBuffer.java --- a/src/org/python/core/PyBuffer.java +++ b/src/org/python/core/PyBuffer.java @@ -30,6 +30,14 @@ // int getLen(); /** + * Return the underlying exporting object (or null if no object implementing the + * {@link BufferProtocol} is in that role). This will often be a PyObject. + * + * @return exporting object (or null) + */ + BufferProtocol getObj(); + + /** * Return the byte indexed from a one-dimensional buffer with item size one. This is part of the * fully-encapsulated API: the buffer implementation exported takes care of navigating the * structure of the buffer. Results are undefined where the number of dimensions is not one or @@ -106,47 +114,46 @@ * further study.) * * @param dest destination byte array - * @param destPos index in the destination array of the byte [0] + * @param destPos byte-index in the destination array of the byte [0] * @throws IndexOutOfBoundsException if the destination cannot hold it */ void copyTo(byte[] dest, int destPos) throws IndexOutOfBoundsException, PyException; /** - * Copy a simple slice of the buffer to the destination byte array, defined by a starting index - * and length in the source buffer. This may validly be done only for a one-dimensional buffer, - * as the meaning of the starting index is otherwise not defined. The length (like the source - * index) is in source buffer items: length*itemsize bytes will be occupied - * in the destination. + * Copy a simple slice of the buffer-view to the destination byte array, defined by a starting + * item-index in the source buffer and the count of items to copy. This may validly + * be done only for a one-dimensional buffer, as the meaning of the starting item-index is + * otherwise not defined. count*itemsize bytes will be occupied in the destination. * - * @param srcIndex starting index in the source buffer + * @param srcIndex starting item-index in the source buffer * @param dest destination byte array - * @param destPos index in the destination array of the item [0,...] - * @param length number of items to copy + * @param destPos byte-index in the destination array of the source item [0,...] + * @param count number of items to copy * @throws IndexOutOfBoundsException if access out of bounds in source or destination */ - void copyTo(int srcIndex, byte[] dest, int destPos, int length) // mimic arraycopy args + void copyTo(int srcIndex, byte[] dest, int destPos, int count) // mimic arraycopy args throws IndexOutOfBoundsException, PyException; /** - * Copy bytes from a slice of a (Java) byte array into the buffer. This may validly be done only - * for a one-dimensional buffer, as the meaning of the starting index is not otherwise defined. - * The length (like the destination index) is in buffer items: - * length*itemsize bytes will be read from the source. + * Copy from a slice of a (Java) byte array into the buffer starting at a given destination + * item-index. This may validly be done only for a one-dimensional buffer, as the meaning of the + * destination index is not otherwise defined. count*itemsize bytes will be read + * from the source. * * @param src source byte array * @param srcPos location in source of first byte to copy - * @param destIndex starting index in the destination (i.e. this) - * @param length number of bytes to copy in + * @param destIndex starting item-index in the destination (i.e. this) + * @param count number of items to copy in * @throws IndexOutOfBoundsException if access out of bounds in source or destination * @throws PyException (TypeError) if read-only buffer */ - void copyFrom(byte[] src, int srcPos, int destIndex, int length) // mimic arraycopy args + void copyFrom(byte[] src, int srcPos, int destIndex, int count) // mimic arraycopy args throws IndexOutOfBoundsException, PyException; /** - * Copy the whole of another PyBuffer into this buffer. This may validly be done only for - * buffers that are consistent in their dimensions. When it is necessary to copy partial - * buffers, this may be achieved using a buffer slice on the source or destination. + * Copy the whole of another PyBuffer into this buffer. This may validly be done + * only for buffers that are consistent in their dimensions. When it is necessary to copy + * partial buffers, this may be achieved using a buffer slice on the source or destination. * * @param src source buffer * @throws IndexOutOfBoundsException if access out of bounds in source or destination @@ -202,10 +209,10 @@ * * @param flags specifying features demanded and the navigational capabilities of the consumer * @param start index in the current buffer - * @param length number of items in the required slice + * @param count number of items in the required slice * @return a buffer representing the slice */ - public PyBuffer getBufferSlice(int flags, int start, int length); + public PyBuffer getBufferSlice(int flags, int start, int count); /** * Get a PyBuffer that represents a slice of the current one described in terms of @@ -217,7 +224,7 @@ * Suppose that x(i) denotes the ith element of the current buffer, that is, the * byte retrieved by this.byteAt(i) or the unit indicated by * this.getPointer(i). A request for a slice where start = s, - * length = N and stride = m, results in a buffer + * count = N and stride = m, results in a buffer * y such that y(k) = x(s+km) where k=0..(N-1). In Python terms, this is * the slice x[s : s+(N-1)m+1 : m] (if m>0) or the slice x[s : s+(N-1)m-1 : * m] (if m<0). Implementations should check that this range is entirely within @@ -226,26 +233,60 @@ * In a simple buffer backed by a contiguous byte array, the result is a strided PyBuffer on the * same storage but where the offset is adjusted by s and the stride is as supplied. If * the current buffer is already strided and/or has an item size larger than single bytes, the - * new start index, length and stride will be translated + * new start index, count and stride will be translated * from the arguments given, through this buffer's stride and item size. The caller always * expresses start and strides in terms of the abstract view of this * buffer. * * @param flags specifying features demanded and the navigational capabilities of the consumer * @param start index in the current buffer - * @param length number of items in the required slice + * @param count number of items in the required slice * @param stride index-distance in the current buffer between consecutive items in the slice * @return a buffer representing the slice */ - public PyBuffer getBufferSlice(int flags, int start, int length, int stride); + public PyBuffer getBufferSlice(int flags, int start, int count, int stride); - // java.nio access to actual storage + // Access to underlying byte-oriented storage // /** + * Convert an item index (for a one-dimensional buffer) to an absolute byte index in the storage + * shared by the exporter. The storage exported as a PyBuffer is a linearly-indexed + * sequence of bytes, although it may not actually be a heap-allocated Java byte[] + * object. The purpose of this method is to allow the exporter to define the relationship + * between the item index (as used in {@link #byteAt(int)}) and the byte-index (as used with the + * ByteBuffer returned by {@link #getNIOByteBuffer()}). See + * {@link #byteIndex(int[])} for discussion of the multi-dimensional case. + * + * @param index item-index from consumer + * @return corresponding byte-index in actual storage + */ + // Should it throw IndexOutOfBoundsException if the index <0 or ≥shape[0]PyBuffer is a linearly-indexed sequence of + * bytes, although it may not actually be a heap-allocated Java byte[] object. The + * purpose of this method is to allow the exporter to define the relationship between the item + * index (as used in {@link #byteAt(int...)} and the byte-index (as used with the + * ByteBuffer returned by {@link #getNIOByteBuffer()}). + * + * @param indices n-dimensional item-index from consumer + * @return corresponding byte-index in actual storage + */ + // Should it throw IndexOutOfBoundsException if any index <0 or ≥shape[i]? + int byteIndex(int... indices); + + /** * Obtain a {@link java.nio.ByteBuffer} giving access to the bytes that hold the data being - * exported to the consumer. For a one-dimensional contiguous buffer, assuming the following - * client code where obj has type BufferProtocol: + * exported by the original object. The position of the buffer is at the first byte of the item + * with zero index (quite possibly not the lowest valid byte-index), the limit of the buffer is + * beyond the largest valid byte index, and the mark is undefined. + *

+ * For a one-dimensional contiguous buffer, the limit is one byte beyond the last item, so that + * consecutive reads from the ByteBuffer return the data in order. Assuming the + * following client code where obj has type BufferProtocol: * *

      * PyBuffer a = obj.getBuffer(PyBUF.SIMPLE);
@@ -253,34 +294,31 @@
      * ByteBuffer bb = a.getNIOBuffer();
      * 
* - * the item with index bb.pos()+k is in the buffer bb at positions + * the item with index k is in bb at positions * bb.pos()+k*itemsize to bb.pos()+(k+1)*itemsize - 1 inclusive. And * if itemsize==1, the item is simply the byte at position bb.pos()+k. - * The buffer limit is set to the first byte beyond the valid data. A block read or write will - * therefore access the contents sequentially. *

* If the buffer is multidimensional or non-contiguous (strided), the buffer position is still - * the (first byte of) the item at index [0] or [0,...,0], and the - * limit is one item beyond the valid data. However, it is necessary to navigate bb - * using the shape, strides and maybe suboffsets provided - * by the API. + * the (first byte of) the item at index [0] or [0,...,0]. However, it + * is necessary to navigate bb using the shape, strides + * and maybe suboffsets provided by the API. * - * @return a ByteBuffer equivalent to the exported data contents. + * @return a ByteBuffer onto the exported data contents. */ ByteBuffer getNIOByteBuffer(); - // Direct access to actual storage - // - /** - * Determine whether the exporter is able to offer direct access to the exported storage as a - * Java byte array (through the API that involves class {@link Pointer}), or only supports the + * Report whether the exporter is able to offer direct access to the exported storage as a Java + * byte array (through the API that involves class {@link Pointer}), or only supports the * abstract API. See also {@link PyBUF#AS_ARRAY}. * * @return true if array access is supported, false if it is not. */ boolean hasArray(); + // Direct access to actual storage (deprecated) + // + /** * A class that references a byte[] array and a particular offset within it, as the * return type for methods that give direct access to byte-oriented data exported by a Python @@ -288,6 +326,7 @@ * this array, and in others not. See {@link PyBuffer#isReadonly()}. It is used by the Jython * buffer API roughly where the CPython buffer API uses a C (char *) pointer. */ + @Deprecated public static class Pointer { /** Reference to the array holding the bytes. */ @@ -379,7 +418,7 @@ * free to navigate the underlying buffer b.storage without respecting these * boundaries. If the buffer is non-contiguous, the above description is still valid (since a * multi-byte item must itself be contiguously stored), but in any additional navigation of - * b.storage[] to other units, the client must use the shape, strides and + * b.storage[] to other items, the client must use the shape, strides and * sub-offsets provided by the API. Normally one starts b = a.getBuf() in order to * establish the offset of index [0,...,0]. * diff --git a/src/org/python/core/PyByteArray.java b/src/org/python/core/PyByteArray.java --- a/src/org/python/core/PyByteArray.java +++ b/src/org/python/core/PyByteArray.java @@ -228,7 +228,7 @@ if (pybuf == null) { // No existing export we can re-use: create a new one - pybuf = new SimpleWritableBuffer(flags, storage, offset, size); + pybuf = new SimpleWritableBuffer(flags, this, storage, offset, size); // Hold a reference for possible re-use export = new WeakReference(pybuf); } diff --git a/src/org/python/core/PyMemoryView.java b/src/org/python/core/PyMemoryView.java --- a/src/org/python/core/PyMemoryView.java +++ b/src/org/python/core/PyMemoryView.java @@ -75,6 +75,13 @@ } } + // @ExposedGet(doc = obj_doc) // Not exposed in Python 2.7 + public PyObject obj() { + checkNotReleased(); + BufferProtocol obj = backing.getObj(); + return (obj instanceof PyObject) ? (PyObject)obj : Py.None; + } + @ExposedGet(doc = format_doc) public String format() { checkNotReleased(); @@ -843,19 +850,18 @@ } } - /* Traverseproc implementation */ @Override public int traverse(Visitproc visit, Object arg) { int retVal; if (backing != null) { if (backing instanceof PyObject) { - retVal = visit.visit((PyObject) backing, arg); + retVal = visit.visit((PyObject)backing, arg); if (retVal != 0) { return retVal; } } else if (backing instanceof Traverseproc) { - retVal = ((Traverseproc) backing).traverse(visit, arg); + retVal = ((Traverseproc)backing).traverse(visit, arg); if (retVal != 0) { return retVal; } @@ -878,11 +884,10 @@ @Override public boolean refersDirectlyTo(PyObject ob) { - if (ob != null && (ob == backing || ob == shape || ob == strides - || ob == suboffsets)) { + if (ob != null && (ob == backing || ob == shape || ob == strides || ob == suboffsets)) { return true; } else if (suboffsets instanceof Traverseproc) { - return ((Traverseproc) suboffsets).refersDirectlyTo(ob); + return ((Traverseproc)suboffsets).refersDirectlyTo(ob); } else { return false; } diff --git a/src/org/python/core/PyString.java b/src/org/python/core/PyString.java --- a/src/org/python/core/PyString.java +++ b/src/org/python/core/PyString.java @@ -204,7 +204,7 @@ * No existing export we can re-use. Return a buffer, but specialised to defer * construction of the buf object, and cache a soft reference to it. */ - pybuf = new SimpleStringBuffer(flags, getString()); + pybuf = new SimpleStringBuffer(flags, this, getString()); export = new SoftReference(pybuf); } return pybuf; diff --git a/src/org/python/core/buffer/Base1DBuffer.java b/src/org/python/core/buffer/Base1DBuffer.java new file mode 100644 --- /dev/null +++ b/src/org/python/core/buffer/Base1DBuffer.java @@ -0,0 +1,117 @@ +package org.python.core.buffer; + +import org.python.core.PyBUF; + +/** + * Base implementation of the Buffer API appropriate to 1-dimensional arrays, of any item size, + * independent of the storage implementation. The description of {@link BaseBuffer} mostly applies. + */ +public abstract class Base1DBuffer extends BaseBuffer { + + /** The strides array for a contiguous 1D byte buffer. */ + protected static final int[] ONE = {1}; + + /** The shape array for a zero length 1D buffer. */ + protected static final int[] ZERO = {0}; + + /** + * Construct an instance of Base1DBuffer in support of a sub-class, specifying the + * 'feature flags', or at least a starting set to be adjusted later. Also specify the navigation + * ( {@link #index0}, number of elements, and {@link #strides} array. These 'feature flags' are + * the features of the buffer exported, not the flags that form the consumer's request. The + * buffer will be read-only unless {@link PyBUF#WRITABLE} is set. {@link PyBUF#FORMAT} is + * implicitly added to the feature flags. + *

+ * To complete initialisation, the sub-class normally must create its own wrapped byte-storage, + * and call {@link #checkRequestFlags(int)} passing the consumer's request flags. + * + * @param featureFlags bit pattern that specifies the features allowed + * @param index0 index into storage of item[0] + * @param size number of elements in the view + * @param strides an array of length 1 providing index stride between successive elements + */ + protected Base1DBuffer(int featureFlags, int index0, int size, int[] strides) { + super(featureFlags, index0, size == 0 ? ZERO : new int[] {size}, strides); + } + + /** + * Construct an instance of Base1DBuffer in support of a sub-class, specifying the + * 'feature flags', or at least a starting set to be adjusted later. Also specify the navigation + * ( {@link #index0}, number of elements, and byte-index distance from one to the next. These + * 'feature flags' are the features of the buffer exported, not the flags that form the + * consumer's request. The buffer will be read-only unless {@link PyBUF#WRITABLE} is set. + * {@link PyBUF#FORMAT} is implicitly added to the feature flags. + *

+ * To complete initialisation, the sub-class normally must create its own wrapped byte-storage, + * and call {@link #checkRequestFlags(int)} passing the consumer's request flags. + * + * @param featureFlags bit pattern that specifies the features allowed + * @param index0 index into storage of item[0] + * @param size number of elements in the view + * @param stride byte-index distance from one element to the next + */ + protected Base1DBuffer(int featureFlags, int index0, int size, int stride) { + this(featureFlags, index0, size, stride == 1 ? ONE : new int[] {stride}); + } + + @Override + protected int getSize() { + return shape[0]; + } + + @Override + public int getLen() { + return shape[0] * getItemsize(); + } + + /** + * {@inheritDoc} + *

+ * Specialised to one-dimensional, possibly strided buffer. + */ + @Override + protected int calcGreatestIndex() { + int stride = strides[0]; + if (stride == 1) { + return index0 + shape[0] - 1; + } else if (stride > 0) { + return index0 + (shape[0] - 1) * stride; + } else { + return index0; + } + } + + /** + * {@inheritDoc} + *

+ * Specialised to one-dimensional, possibly strided buffer. + */ + @Override + protected int calcLeastIndex() { + int stride = strides[0]; + if (stride < 0) { + return index0 + (shape[0] - 1) * stride; + } else { + return index0; + } + } + + /** + * {@inheritDoc} + *

+ * Specialised in BaseArrayBuffer to one dimension. + */ + @Override + public boolean isContiguous(char order) { + if ("CFA".indexOf(order) < 0) { + return false; + } else { + if (getShape()[0] < 2) { + return true; + } else { + return getStrides()[0] == getItemsize(); + } + } + } + +} diff --git a/src/org/python/core/buffer/BaseArrayBuffer.java b/src/org/python/core/buffer/BaseArrayBuffer.java new file mode 100644 --- /dev/null +++ b/src/org/python/core/buffer/BaseArrayBuffer.java @@ -0,0 +1,228 @@ +package org.python.core.buffer; + +import java.nio.ByteBuffer; + +import org.python.core.PyBUF; +import org.python.core.PyBuffer; +import org.python.core.PyException; + +/** + * Base implementation of the Buffer API for when the storage implementation is byte[]. + * The description of {@link BaseBuffer} mostly applies. Methods provided or overridden here are + * appropriate to 1-dimensional arrays, of any item size, backed by byte[]. + */ +public abstract class BaseArrayBuffer extends Base1DBuffer { + + /** + * Reference to the underlying byte[] storage that the exporter is sharing with the + * consumer. The data need not occupy the whole array: in the constructor of a particular type + * of buffer, the exporter usually indicates an offset to the first significant byte and length + * (contiguous cases) or the index in storage that should be treated as the item + * with index zero (retrieved say by buf.byteAt(0)). + */ + protected byte[] storage; + + /** + * Construct an instance of BaseArrayBuffer in support of a sub-class, specifying + * the 'feature flags', or at least a starting set to be adjusted later. Also specify the + * navigation ( {@link #index0}, number of elements, and stride. These 'feature flags' are the + * features of the buffer exported, not the flags that form the consumer's request. The buffer + * will be read-only unless {@link PyBUF#WRITABLE} is set. {@link PyBUF#FORMAT} and + * {@link PyBUF#AS_ARRAY} are implicitly added to the feature flags. + *

+ * To complete initialisation, the sub-class normally must call {@link #checkRequestFlags(int)} + * passing the consumer's request flags. + * + * @param storage the array of bytes storing the implementation of the exporting object + * @param featureFlags bit pattern that specifies the features allowed + * @param index0 index into storage of item[0] + * @param size number of elements in the view + * @param stride byte-index distance from one element to the next + */ + protected BaseArrayBuffer(byte[] storage, int featureFlags, int index0, int size, int stride) { + super(featureFlags | AS_ARRAY, index0, size, stride); + this.storage = storage; + } + + @Override + protected byte byteAtImpl(int byteIndex) throws IndexOutOfBoundsException { + return storage[byteIndex]; + } + + @Override + protected void storeAtImpl(byte value, int byteIndex) throws IndexOutOfBoundsException, + PyException { + checkWritable(); + storage[byteIndex] = value; + } + + @Override + public int byteIndex(int... indices) throws IndexOutOfBoundsException { + // BaseBuffer implementation can be simplified since if indices.length!=1 we error. + checkDimension(indices.length); // throws if != 1 + return byteIndex(indices[0]); + } + + /** + * {@inheritDoc} + *

+ * The implementation in BaseArrayBuffer deals with the general one-dimensional + * case of arbitrary item size and stride. + */ + @Override + public void copyTo(int srcIndex, byte[] dest, int destPos, int count) + throws IndexOutOfBoundsException { + + if (count > 0) { + // Pick up attributes necessary to choose an efficient copy strategy + int itemsize = getItemsize(); + int stride = getStrides()[0]; + int skip = stride - itemsize; + int s = byteIndex(srcIndex); + + // Strategy depends on whether items are laid end-to-end contiguously or there are gaps + if (skip == 0) { + // stride == itemsize: straight copy of contiguous bytes + System.arraycopy(storage, s, dest, destPos, count * itemsize); + } else { + int limit = s + count * stride, d = destPos; + if (itemsize == 1) { + // Non-contiguous copy: single byte items + for (; s != limit; s += stride) { + dest[d++] = storage[s]; + } + } else { + // Non-contiguous copy: each time, copy itemsize bytes then skip + for (; s != limit; s += skip) { + int t = s + itemsize; + while (s < t) { + dest[d++] = storage[s++]; + } + } + } + } + } + } + + /** + * {@inheritDoc} + *

+ * The default implementation in BaseArrayBuffer deals with the general + * one-dimensional case of arbitrary item size and stride. + */ + @Override + public void copyFrom(byte[] src, int srcPos, int destIndex, int count) + throws IndexOutOfBoundsException, PyException { + copyFrom(src, srcPos, 1, destIndex, count); + } + + /** + * Generalisation of {@link PyBuffer#copyFrom(byte[], int, int, int)} to allow a stride within + * the source array. + * + * @param src source byte array + * @param srcPos byte-index location in source of first byte to copy + * @param srcStride byte-index increment from one item to the next + * @param destIndex starting item-index in the destination (i.e. this) + * @param count number of items to copy in + * @throws IndexOutOfBoundsException if access out of bounds in source or destination + * @throws PyException (TypeError) if read-only buffer + */ + protected void copyFrom(byte[] src, int srcPos, int srcStride, int destIndex, int count) + throws IndexOutOfBoundsException, PyException { + + checkWritable(); + + if (count > 0) { + // Pick up attributes necessary to choose an efficient copy strategy + int itemsize = getItemsize(); + int stride = getStrides()[0]; + int skip = stride - itemsize; + int d = byteIndex(destIndex); + + int srcSkip = srcStride - itemsize; + + // Strategy depends on whether items are laid end-to-end or there are gaps + if (skip == 0 && srcSkip == 0) { + // Straight copy of contiguous bytes + System.arraycopy(src, srcPos, storage, d, count * itemsize); + } else { + int limit = d + count * stride, s = srcPos; + if (itemsize == 1) { + // Non-contiguous copy: single byte items + for (; d != limit; d += stride) { + storage[d] = src[s]; + s += srcStride; + } + } else { + // Non-contiguous copy: itemsize bytes then skip to next item + for (; d != limit; d += skip) { + int t = d + itemsize; + while (d < t) { + storage[d++] = src[s++]; + } + s += srcSkip; + } + } + } + } + } + + @Override + public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { + if (src instanceof BaseArrayBuffer && !this.overlaps((BaseArrayBuffer)src)) { + // We can do this efficiently, copying between arrays. + copyFromArrayBuffer((BaseArrayBuffer)src); + } else { + super.copyFrom(src); + } + } + + private boolean overlaps(BaseArrayBuffer src) { + if (src.storage != this.storage) { + return false; + } else { + int low = calcLeastIndex(), high = calcGreatestIndex(); + int srcLow = src.calcLeastIndex(), srcHigh = src.calcGreatestIndex(); + return (srcHigh >= low && high >= srcLow); + } + } + + private void copyFromArrayBuffer(BaseArrayBuffer src) throws IndexOutOfBoundsException, + PyException { + + src.checkDimension(1); + + int itemsize = getItemsize(); + int count = getSize(); + + // Block operation if different item or overall size + if (src.getItemsize() != itemsize || src.getSize() != count) { + throw differentStructure(); + } + + // We depend on the striding copyFrom() acting directly on the source storage + copyFrom(src.storage, src.index0, src.strides[0], 0, count); + } + + @Override + protected ByteBuffer getNIOByteBufferImpl() { + // The buffer spans the whole storage, which may include data not in the view + ByteBuffer b = ByteBuffer.wrap(storage); + // Return as read-only if it is. + return isReadonly() ? b.asReadOnlyBuffer() : b; + } + + /** + * {@inheritDoc} + *

+ * BaseArrayBuffer provides a reference to the storage array even when the buffer + * is intended not to be writable. There can be no enforcement of read-only character once a + * reference to the byte array has been handed out. + */ + @SuppressWarnings("deprecation") + @Override + public Pointer getBuf() { + return new Pointer(storage, index0); + } +} diff --git a/src/org/python/core/buffer/BaseBuffer.java b/src/org/python/core/buffer/BaseBuffer.java --- a/src/org/python/core/buffer/BaseBuffer.java +++ b/src/org/python/core/buffer/BaseBuffer.java @@ -9,34 +9,35 @@ import org.python.core.PyException; /** - * Base implementation of the Buffer API providing variables and accessors for the navigational + * Base implementation of the Buffer API providing variables and accessors for the navigation * arrays, methods for expressing and checking the buffer request flags, methods and mechanism for * get-release counting, boilerplate error checks and their associated exceptions, and default * implementations of some methods for access to the buffer content. The design aim is to ensure * unglamorous common code need only be implemented once. *

- * Where provided, the buffer access methods are appropriate to 1-dimensional arrays where the units - * are single bytes, stored contiguously. Sub-classes that deal with N-dimensional arrays, - * non-contiguous storage and items that are not single bytes must override the default - * implementations. + * This class leaves undefined the storage mechanism for the bytes (typically byte[] or + * java.nio.ByteBuffer), while remaining definite that it is an indexable sequence of + * bytes. A concrete class that extends this one must provide elementary accessors + * {@link #byteAtImpl(int)}, {@link #storeAtImpl(byte, int)} that abstract this storage, a factory + * {@link #getNIOByteBufferImpl()} for ByteBuffers that wrap the storage, and a factory + * for slices {@link #getBufferSlice(int, int, int, int)}. + *

+ * The sub-class constructor must specify the feature flags (see {@link #BaseBuffer(int)}), set + * {@link #index0}, {@link #shape} and {@link #strides}, and finally check the client capabilities + * with {@link #checkRequestFlags(int)}. Sub-classes intended to represent slices of exporters that + * must count their exports as part of a locking protocol, as does bytearray, must + * override {@link #getRoot()} so that a buffer view {@link #release()} on a slice, propagates to + * the buffer view that provided it. + *

+ * Access methods provided here necessarily work with the abstracted {@link #byteAtImpl(int)}, + * {@link #storeAtImpl(byte, int)} interface, but subclasses are able to override them with more + * efficient versions that employ knowledge of the particular storage type used. *

* This base implementation is writable only if {@link PyBUF#WRITABLE} is in the feature flags - * passed to the constructor. Otherwise, all methods for write access raise a - * BufferError read-only exception and {@link #isReadonly()} returns true. - * Sub-classes can follow the same pattern, setting {@link PyBUF#WRITABLE} in the constructor and, - * if they have to, overriding the operations that write (storeAt and - * copyFrom). The recommended pattern is: - * - *

- * if (isReadonly()) {
- *     throw notWritable();
- * }
- * // ... implementation of the write operation
- * 
- * Another approach, used in the standard library, is to have distinct classes for the writable and - * read-only variants. The implementors of simple buffers will find it efficient to override the - * generic access methods to which performance might be sensitive, with a calculation specific to - * their actual type. + * passed to the constructor. Otherwise, all methods for write access raise a TypeError + * and {@link #isReadonly()} returns true. However, a client intending to write should + * have presented {@link PyBUF#WRITABLE} in its client request flags when getting the buffer, and + * been prevented by a BufferError exception at that point. *

* At the time of writing, only one-dimensional buffers of item size one are used in the Jython * core. @@ -44,6 +45,12 @@ public abstract class BaseBuffer implements PyBuffer { /** + * The object that exported this buffer (or null if the subclass or exporter + * chooses not to supply a reference). + */ + protected BufferProtocol obj; + + /** * The dimensions of the array represented by the buffer. The length of the shape * array is the number of dimensions. The shape array should always be created and * filled (difference from CPython). This value is returned by {@link #getShape()}. @@ -59,19 +66,11 @@ protected int[] strides; /** - * Reference to the underlying byte[] storage that the exporter is sharing with the - * consumer. The data need not occupy the whole array: in the constructor of a particular type - * of buffer, the exporter usually indicates an offset to the first significant byte and length - * (contiguous cases) or the index in storage that should be treated as the item - * with index zero (retrieved say by {@link #byteAt(int)}). - */ - protected byte[] storage; - - /** - * Absolute index in storage of item[0]. In one dimension, for a - * positive stride this is equal to the offset of the first byte used in - * {@link #storage}, and for a negative stride it is the last. In an N-dimensional - * buffer with strides of mixed sign, it could be anywhere in the data. + * Absolute byte-index in the storage of item[0]. In one dimension, for a positive + * stride this is equal to the offset of the first byte used in whatever + * byte-storage is provided, and for a negative stride it is the first byte of the + * last item. In an N-dimensional buffer with strides of mixed sign, it could be anywhere in the + * data. */ protected int index0; @@ -91,8 +90,8 @@ * the consumer does not specify that it will use a navigation array the buffer requires. *

* In order to support efficient checking with {@link #checkRequestFlags(int)} we store a - * mutilated version of the apparent featureFlags in which the non-navigational - * flags are inverted. The syndrome S of the error is computed as follows. Let + * mutilated version of the apparent featureFlags in which the non-navigation flags + * are inverted. The syndrome S of the error is computed as follows. Let * N=1 where we are dealing with a navigation flag, let F be a buffer * feature flag, and let X be the consumer request flags. * @@ -125,20 +124,26 @@ private int gFeatureFlags = ~NAVIGATION; // featureFlags = 0 /** - * Construct an instance of BaseBuffer in support of a sub-class, specifying the 'feature - * flags', or at least a starting set to be adjusted later. These are the features of the buffer - * exported, not the flags that form the consumer's request. The buffer will be read-only unless - * {@link PyBUF#WRITABLE} is set in the feature flags. {@link PyBUF#FORMAT} and - * {@link PyBUF#AS_ARRAY} are implicitly added to the feature flags. The navigation arrays are - * all null, awaiting action by the sub-class constructor. To complete initialisation, the - * sub-class normally must assign: the buffer ( {@link #storage}, {@link #index0}), and the - * navigation arrays ({@link #shape}, {@link #strides}), and call - * {@link #checkRequestFlags(int)} passing the consumer's request flags. + * Construct an instance of BaseBuffer in support of a sub-class, specifying the + * 'feature flags', or at least a starting set to be adjusted later. Also specify the navigation + * ( {@link #index0}, {@link #shape}, and {@link #strides}). These 'feature flags' are the + * features of the buffer exported, not the flags that form the consumer's request. The buffer + * will be read-only unless {@link PyBUF#WRITABLE} is set. {@link PyBUF#FORMAT} is implicitly + * added to the feature flags. + *

+ * To complete initialisation, the sub-class normally must create its own wrapped byte-storage, + * and call {@link #checkRequestFlags(int)} passing the consumer's request flags. * - * @param featureFlags bit pattern that specifies the actual features allowed/required + * @param featureFlags bit pattern that specifies the features allowed + * @param index0 index into storage of item[0,...,0] + * @param shape elements in each dimension + * @param strides between successive elements in each dimension */ - protected BaseBuffer(int featureFlags) { - setFeatureFlags(featureFlags | FORMAT | AS_ARRAY); + protected BaseBuffer(int featureFlags, int index0, int[] shape, int[] strides) { + setFeatureFlags(featureFlags | FORMAT); + this.index0 = index0; + this.shape = shape; + this.strides = strides; } /** @@ -175,6 +180,17 @@ } /** + * Remove features from this buffer expressed using the constants defined in {@link PyBUF}, + * clearing individual flags specified while leaving others already set. Equivalent to + * setFeatureFlags(~flags & getFeatureFlags()). + * + * @param flags to clear within the feature flags + */ + protected final void removeFeatureFlags(int flags) { + setFeatureFlags(~flags & getFeatureFlags()); + } + + /** * General purpose method to check the consumer request flags (typically the argument to * {@link BufferProtocol#getBuffer(int)}) against the feature flags (see * {@link #getFeatureFlags()}) that characterise the features of the buffer, and to raise an @@ -187,7 +203,7 @@ * capabilities this type (or instance) buffer actually has. It is an error, for the consumer to * specify in its request a feature that the buffer does not offer. *

- * In a subset of the flags, the consumer specifies the set of navigational arrays ( + * In a subset of the flags, the consumer specifies the set of navigation arrays ( * shape, strides, and suboffsets) it intends to use in * navigating the buffer. When the buffer implementation calls this check method, it has already * specified in {@link #setFeatureFlags(int)} what navigation is necessary for the consumer to @@ -210,14 +226,8 @@ @Override public boolean isReadonly() { - // WRITABLE is a non-navigational flag, so is inverted in gFeatureFlags - return (gFeatureFlags & WRITABLE) != 0; - } - - @Override - public boolean hasArray() { - // AS_ARRAY is a non-navigational flag, so is inverted in gFeatureFlags - return (gFeatureFlags & AS_ARRAY) != 0; + // WRITABLE is a non-navigation flag, so is inverted in gFeatureFlags + return (gFeatureFlags & WRITABLE) != 0; // i.e. featureFlags & WRITABLE is false } @Override @@ -231,91 +241,155 @@ return shape; } + // XXX Consider making this part of the PyBUF interface + protected int getSize() { + final int N = shape.length; + int size = shape[0]; + for (int k = 1; k < N; k++) { + size *= shape[k]; + } + return size; + } + + @Override + public int getLen() { + final int N = shape.length; + int len = getItemsize(); + for (int k = 0; k < N; k++) { + len *= shape[k]; + } + return len; + } + + @Override + public final BufferProtocol getObj() { + return obj; + } + + /** + * Retrieve the byte at the given index in the underlying storage treated as a flat sequence of + * bytes. This byte-index will have been computed from the item index (which may have been + * multi-dimensional), taking into account {@link #index0}, {@link #shape}, {@link #strides}, + * and the item size. The caller is responsible for validating the original item-index and + * raising (typically) an IndexOutOfBoundsException. Misuse of this method may + * still result in unchecked exceptions characteristic of the storage implementation. + * + * @param byteIndex byte-index of location to retrieve + * @return the byte at byteIndex + */ + abstract protected byte byteAtImpl(int byteIndex) throws IndexOutOfBoundsException; + + /** + * Store the byte at the given index in the underlying storage treated as a flat sequence of + * bytes. This byte-index will have been computed from the item index (which may have been + * multi-dimensional), taking into account {@link #index0}, {@link #shape}, {@link #strides}, + * and the item size. The caller is responsible for validating the original item-index and + * raising (typically) an IndexOutOfBoundsException. Misuse of this method may + * still result in unchecked exceptions characteristic of the storage implementation. This + * method must implement the check for read-only character, raising a BufferError + * in the case of a violation. + * + * @param value to store + * @param byteIndex byte-index of location to retrieve + * @throws PyException(BufferError) if this object is read-only. + */ + abstract protected void storeAtImpl(byte value, int byteIndex) + throws IndexOutOfBoundsException, PyException; + /** * {@inheritDoc} *

- * The default implementation in BaseBuffer deals with the general one-dimensional - * case, with any item size and stride. + * The BaseBuffer implementation delegates to {@link #byteAtImpl(int)} via + * byteAtImpl(byteIndex(index)). */ @Override - public int getLen() { - // Correct if one-dimensional. Override with itemsize*product(shape). - return shape[0] * getItemsize(); - } - - @Override public byte byteAt(int index) throws IndexOutOfBoundsException { - return storage[calcIndex(index)]; - } - - @Override - public int intAt(int index) throws IndexOutOfBoundsException { - return 0xff & byteAt(index); - } - - @Override - public void storeAt(byte value, int index) throws IndexOutOfBoundsException, PyException { - if (isReadonly()) { - throw notWritable(); - } - storage[calcIndex(index)] = value; + return byteAtImpl(byteIndex(index)); } /** - * Convert an item index (for a one-dimensional buffer) to an absolute byte index in the actual - * storage being shared by the exporter. See {@link #calcIndex(int...)} for discussion. - * - * @param index from consumer - * @return index in actual storage + * {@inheritDoc} + *

+ * The BaseBuffer implementation delegates to {@link #byteAtImpl(int)} via + * byteAtImpl(byteIndex(index)), cast unsigned to an int. */ - protected int calcIndex(int index) throws IndexOutOfBoundsException { - // Treat as one-dimensional - return index0 + index * getStrides()[0]; + @Override + public int intAt(int index) throws IndexOutOfBoundsException { + return 0xff & byteAtImpl(byteIndex(index)); } + /** + * {@inheritDoc} + *

+ * The BaseBuffer implementation delegates to {@link #storeAtImpl(byte, int)} via + * storeAtImpl(value, byteIndex(index)). + */ + @Override + public void storeAt(byte value, int index) throws IndexOutOfBoundsException, PyException { + storeAtImpl(value, byteIndex(index)); + } + + /** + * {@inheritDoc} + *

+ * The BaseBuffer implementation delegates to {@link #byteAtImpl(int)} via + * byteAtImpl(byteIndex(indices)). + */ @Override public byte byteAt(int... indices) throws IndexOutOfBoundsException { - return storage[calcIndex(indices)]; + return byteAtImpl(byteIndex(indices)); } + /** + * {@inheritDoc} + *

+ * The BaseBuffer implementation delegates to {@link #byteAtImpl(int)} via + * byteAtImpl(byteIndex(indices)), cast unsigned to an int. + */ @Override public int intAt(int... indices) throws IndexOutOfBoundsException { return 0xff & byteAt(indices); } + /** + * {@inheritDoc} + *

+ * The BaseBuffer implementation delegates to {@link #storeAtImpl(byte, int)} via + * storeAtImpl(value, byteIndex(indices)). + */ @Override public void storeAt(byte value, int... indices) throws IndexOutOfBoundsException, PyException { - if (isReadonly()) { - throw notWritable(); - } - storage[calcIndex(indices)] = value; + storeAtImpl(value, byteIndex(indices)); } - /** - * Convert a multi-dimensional item index (if we are not using indirection) to an absolute byte - * index in the actual storage array being shared by the exporter. The purpose of this method is - * to allow a sub-class to define, in one place, an indexing calculation that maps the index as - * provided by the consumer into an index in the storage known to the buffer. - *

- * In the usual case where the storage is referenced via the {@link #storage} and - * {@link #index0} members, the buffer implementation may use storage[calcIndex(i)] - * to reference the (first byte of) the item x[i]. This is what the default implementation of - * accessors in BaseBuffer will do. In the simplest cases, calling - * calcIndex may be an overhead to avoid, and an implementation will specialise the - * accessors. The default implementation here is suited to N-dimensional arrays. - * - * @param indices of the item from the consumer - * @return corresponding absolute index in storage + /* + * In this implementation, we throw IndexOutOfBoundsException if index < 0 or > shape[0], but we + * could rely on the array or ByteBuffer checks when indexing, especially the latter since + * position is checked against limit. */ - protected int calcIndex(int... indices) throws IndexOutOfBoundsException { + @Override + public int byteIndex(int index) throws IndexOutOfBoundsException { + // Treat as one-dimensional + if (index < 0 || index >= shape[0]) { + throw new IndexOutOfBoundsException(); + } + return index0 + index * strides[0]; + } + + /* + * In this implementation, we throw IndexOutOfBoundsException if any index[i] < 0 or > shape[i]. + */ + @Override + public int byteIndex(int... indices) throws IndexOutOfBoundsException { final int N = checkDimension(indices); // In general: index0 + sum(k=0,N-1) indices[k]*strides[k] int index = index0; - if (N > 0) { - int[] strides = getStrides(); - for (int k = 0; k < N; k++) { - index += indices[k] * strides[k]; + for (int k = 0; k < N; k++) { + int ik = indices[k]; + if (ik < 0 || ik >= shape[k]) { + throw new IndexOutOfBoundsException(); } + index += ik * strides[k]; } return index; } @@ -323,24 +397,22 @@ /** * Calculate the absolute byte index in the storage array of the last item of the exported data * (if we are not using indirection). This is the greatest value attained by - * {@link #calcIndex(int...)}. The first byte not used will be one itemsize more + * {@link #byteIndex(int...)}. The first byte not used will be one itemsize more * than the returned value. * * @return greatest absolute index in storage */ - protected int calcGreatestIndex() throws IndexOutOfBoundsException { + protected int calcGreatestIndex() { final int N = shape.length; // If all the strides are positive, the maximal value is found from: // index = index0 + sum(k=0,N-1) (shape[k]-1)*strides[k] // but in general, for any k where strides[k]<=0, the term should be zero. int index = index0; - if (N > 0) { - int[] strides = getStrides(); - for (int k = 0; k < N; k++) { - int stride = strides[k]; - if (stride > 0) { - index += (shape[k] - 1) * stride; - } + int[] strides = getStrides(); + for (int k = 0; k < N; k++) { + int stride = strides[k]; + if (stride > 0) { + index += (shape[k] - 1) * stride; } } return index; @@ -349,23 +421,21 @@ /** * Calculate the absolute byte index in the storage array of the first item of the exported data * (if we are not using indirection). This is the least value attained by - * {@link #calcIndex(int...)}. + * {@link #byteIndex(int...)}. * * @return least absolute index in storage */ - protected int calcLeastIndex() throws IndexOutOfBoundsException { + protected int calcLeastIndex() { final int N = shape.length; // If all the strides are positive, the maximal value is just index0, // but in general, we must allow strides[k]<=0 for some k: // index = index0 + sum(k=0,N-1) (strides[k]<0) ? (shape[k]-1)*strides[k] : 0 int index = index0; - if (N > 0) { - int[] strides = getStrides(); - for (int k = 0; k < N; k++) { - int stride = strides[k]; - if (stride < 0) { - index += (shape[k] - 1) * stride; - } + int[] strides = getStrides(); + for (int k = 0; k < N; k++) { + int stride = strides[k]; + if (stride < 0) { + index += (shape[k] - 1) * stride; } } return index; @@ -380,147 +450,101 @@ @Override public void copyTo(byte[] dest, int destPos) throws IndexOutOfBoundsException { // Note shape[0] is the number of items in the array - copyTo(0, dest, destPos, shape[0]); + copyTo(0, dest, destPos, getSize()); } /** * {@inheritDoc} *

* The default implementation in BaseBuffer deals with the general one-dimensional - * case of arbitrary item size and stride. + * case of arbitrary item size and stride, but is unable to optimise access to sequential bytes. */ @Override - public void copyTo(int srcIndex, byte[] dest, int destPos, int length) - throws IndexOutOfBoundsException { + public void copyTo(int srcIndex, byte[] dest, int destPos, int count) + throws IndexOutOfBoundsException, PyException { - // Data is here in the buffers - int s = calcIndex(srcIndex); - int d = destPos; + checkDimension(1); - // Pick up attributes necessary to choose an efficient copy strategy int itemsize = getItemsize(); - int stride = getStrides()[0]; - int skip = stride - itemsize; + int s = srcIndex, d = destPos; - // Strategy depends on whether items are laid end-to-end contiguously or there are gaps - if (skip == 0) { - // stride == itemsize: straight copy of contiguous bytes - System.arraycopy(storage, s, dest, d, length * itemsize); - - } else if (itemsize == 1) { - // Non-contiguous copy: single byte items - int limit = s + length * stride; - for (; s < limit; s += stride) { - dest[d++] = storage[s]; + if (itemsize == 1) { + // Single byte items + for (int i = 0; i < count; i++) { + dest[d++] = byteAt(s++); } - } else { - // Non-contiguous copy: each time, copy itemsize bytes then skip - int limit = s + length * stride; - for (; s < limit; s += skip) { - int t = s + itemsize; - while (s < t) { - dest[d++] = storage[s++]; + // Multi-byte items + for (int i = 0; i < count; i++) { + int p = byteIndex(s++); + for (int j = 0; j < itemsize; j++) { + dest[d++] = byteAtImpl(p + j); } } } - } /** * {@inheritDoc} *

* The default implementation in BaseBuffer deals with the general one-dimensional - * case of arbitrary item size and stride. + * case of arbitrary item size and stride, but is unable to optimise access to sequential bytes. */ @Override - public void copyFrom(byte[] src, int srcPos, int destIndex, int length) + public void copyFrom(byte[] src, int srcPos, int destIndex, int count) throws IndexOutOfBoundsException, PyException { - // Block operation if read-only - if (isReadonly()) { - throw notWritable(); - } + checkDimension(1); + checkWritable(); - // Data is here in the buffers - int s = srcPos; - int d = calcIndex(destIndex); + int itemsize = getItemsize(); + int d = destIndex, s = srcPos; - // Pick up attributes necessary to choose an efficient copy strategy - int itemsize = getItemsize(); - int stride = getStrides()[0]; - int skip = stride - itemsize; - - // Strategy depends on whether items are laid end-to-end or there are gaps - if (skip == 0) { - // Straight copy of contiguous bytes - System.arraycopy(src, srcPos, storage, d, length * itemsize); - - } else if (itemsize == 1) { - // Non-contiguous copy: single byte items - int limit = d + length * stride; - for (; d != limit; d += stride) { - storage[d] = src[s++]; + if (itemsize == 1) { + // Single byte items + for (int i = 0; i < count; i++) { + storeAt(src[s++], d++); } - } else { - // Non-contiguous copy: each time, copy itemsize bytes then skip - int limit = d + length * stride; - for (; d != limit; d += skip) { - int t = d + itemsize; - while (d < t) { - storage[d++] = src[s++]; + // Multi-byte items + for (int i = 0; i < count; i++) { + int p = byteIndex(d++); + for (int j = 0; j < itemsize; j++) { + storeAtImpl(src[s++], p++); } } } - } /** * {@inheritDoc} *

* The default implementation in BaseBuffer deals with the general one-dimensional - * case. + * case of arbitrary item size and stride, but is unable to optimise access to sequential bytes. */ @Override public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { - // Block operation if read-only and same length - if (isReadonly()) { - throw notWritable(); - } else if (src.getLen() != getLen() || src.getItemsize() != getItemsize()) { + checkDimension(1); + checkWritable(); + + int itemsize = getItemsize(); + int count = getSize(); + int byteLen = src.getLen(); + + // Block operation if different item or overall size (permit reshape) + if (src.getItemsize() != itemsize || byteLen != count * itemsize) { throw differentStructure(); } - // Data is here in the buffers - int s = 0; - int d = calcIndex(0); - - // Pick up attributes necessary to choose an efficient copy strategy - int itemsize = getItemsize(); - int stride = getStrides()[0]; - - // Strategy depends on whether items are laid end-to-end or there are gaps - if (stride == itemsize) { - // Straight copy to contiguous bytes - src.copyTo(storage, d); - - } else if (itemsize == 1) { - // Non-contiguous copy: single byte items - int limit = d + src.getLen() * stride; - for (; d != limit; d += stride) { - storage[d] = src.byteAt(s++); - } - - } else { - // Non-contiguous copy: each time, copy itemsize bytes then skip - int limit = d + src.getShape()[0] * stride; - for (; d != limit; d += stride) { - Pointer srcItem = src.getPointer(s++); - System.arraycopy(srcItem.storage, srcItem.offset, storage, d, itemsize); - } - } - + /* + * It is not possible in general to know that this and src do not share storage. There is + * always a risk of incorrect results if we do not go via an intermediate byte array. + * Sub-classes may be able to avoid this. + */ + byte[] t = new byte[byteLen]; + src.copyTo(t, 0); + this.copyFrom(t, 0, 0, count); } @Override @@ -535,7 +559,7 @@ } /** - * Allow an exporter to re-use a BaseBytes even if it has been "finally" released. Many + * Allow an exporter to re-use this object again even if it has been "finally" released. Many * sub-classes of BaseBytes can be re-used even after a final release by consumers, * simply by incrementing the exports count again: the navigation arrays and the * buffer view of the exporter's state all remain valid. We do not let consumers do this through @@ -567,12 +591,22 @@ * equals the number of getBuffer calls), the implementation here calls * {@link #releaseAction()}, which the implementer of a specific buffer type should override if * it needs specific actions to take place. + *

+ * Note that, when this is a sliced view obtained from another PyBuffer the + * implementation in BaseBuffer automatically sends one release() + * Sub-classes should not propagate the release themselves when overriding + * {@link #releaseAction()}. */ @Override public void release() { if (--exports == 0) { // This is a final release. releaseAction(); + // We have to release the root too if we are not a root. + PyBuffer root = getRoot(); + if (root != this) { + root.release(); + } } else if (exports < 0) { // Buffer already had 0 exports. (Put this right, in passing.) exports = 0; @@ -591,35 +625,63 @@ } @Override - public PyBuffer getBufferSlice(int flags, int start, int length) { - return getBufferSlice(flags, start, length, 1); + public PyBuffer getBufferSlice(int flags, int start, int count) { + return getBufferSlice(flags, start, count, 1); } // Let the sub-class implement - // @Override public PyBuffer getBufferSlice(int flags, int start, int length, int stride) {} + // @Override public PyBuffer getBufferSlice(int flags, int start, int count, int stride) {} + + /** + * Create a new java.nio.ByteBuffer on the underlying storage, such that + * positioning this buffer to a particular byte using {@link #byteIndex(int)} or + * {@link #byteIndex(int[])} positions it at the first byte of the item so indexed. + */ + abstract protected ByteBuffer getNIOByteBufferImpl(); @Override public ByteBuffer getNIOByteBuffer() { - // Determine the limit of the buffer just beyond the last item. - int length = calcGreatestIndex() + getItemsize() - index0; - ByteBuffer b = ByteBuffer.wrap(storage, index0, length); - // Return as read-only if it is. - return isReadonly() ? b.asReadOnlyBuffer() : b; + // The buffer spans the whole storage + ByteBuffer b = getNIOByteBufferImpl(); + // For the one-dimensional contiguous case it makes sense to set the limit: + if (shape.length == 1 && isContiguous('A')) { + int stride = strides[0]; + if (getItemsize() == stride) { + b.limit(index0 + shape[0] * stride); + } + } + // The buffer is positioned at item[0] + b.position(index0); + return b; } @Override - public Pointer getBuf() { - return new Pointer(storage, index0); + public boolean hasArray() { + // AS_ARRAY is a non-navigation flag, so is inverted in gFeatureFlags + return (gFeatureFlags & AS_ARRAY) == 0; // i.e. featureFlags & AS_ARRAY is true } + @SuppressWarnings("deprecation") + @Override + public Pointer getBuf() { + checkHasArray(); + return new Pointer(getNIOByteBuffer().array(), index0); + } + + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int index) throws IndexOutOfBoundsException { - return new Pointer(storage, calcIndex(index)); + Pointer p = getBuf(); + p.offset = byteIndex(index); + return p; } + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int... indices) throws IndexOutOfBoundsException { - return new Pointer(storage, calcIndex(indices)); + Pointer p = getBuf(); + p.offset = byteIndex(indices); + return p; } @Override @@ -633,10 +695,73 @@ return null; } + private boolean isCContiguous() { + /* + * If we were to compute the strides array for a C-contiguous array, the last stride would + * equal the item size, and generally stride[k-1] = shape[k]*stride[k]. This is the basis of + * the test. However, note that for any k where shape[k]==1 there is no "next sub-array" and + * no discontiguity. + */ + final int N = shape.length; + /* + * size is the stride in bytes-index from item[i0,i1,...,ik,0,...,0] to + * item[i0,i1,...,ik+1,0,...,0]. Start the iteration at the largest k. An increment of one + * in the last index makes a stride of the item size. + */ + int size = getItemsize(); + for (int k = N - 1; k >= 0; k--) { + int nk = shape[k]; + if (nk > 1) { + if (strides[k] != size) { + return false; + } + size *= nk; + } + } + return true; + } + + private boolean isFortranContiguous() { + /* + * If we were to compute the strides array for a Fortran-contiguous array, the first stride + * would equal the item size, and generally stride[k+1] = shape[k]*stride[k]. This is the + * basis of the test. However, note that for any k where shape[k]==1 there is no + * "next sub-array" and no discontiguity. + */ + final int N = shape.length; + /* + * size is the stride in bytes-index from item[0,...,0,ik,0,...,0] to + * item[0,...,0,ik+1,0,...,0]. Start the iteration at k=0. An increment of one in the first + * index makes a stride of the item size. + */ + int size = getItemsize(); + for (int k = 0; k < N; k++) { + int nk = shape[k]; + if (nk > 1) { + if (strides[k] != size) { + return false; + } + size *= nk; + } + } + return true; + } + @Override public boolean isContiguous(char order) { - // Correct for one-dimensional buffers - return true; + if (getSuboffsets() != null) { + return false; + } + switch (order) { + case 'C': + return isCContiguous(); + case 'F': + return isFortranContiguous(); + case 'A': + return isCContiguous() || isFortranContiguous(); + default: + return false; + } } @Override @@ -662,9 +787,9 @@ protected void releaseAction() {} /** - * Some PyBuffers, those created by slicing a PyBuffer are related to + * Some PyBuffers, those created by slicing a PyBuffer, are related to * a root PyBuffer. During creation of such a slice, we need to supply a value for - * this root. If the present object is not itself a slice, this is root is the object itself; if + * this root. If the present object is not itself a slice, this root is the object itself; if * the buffer is already a slice, it is the root it was given at creation time. Often this is * the only difference between a slice-view and a directly-exported buffer. Override this method * in slices to return the root buffer of the slice. @@ -676,6 +801,20 @@ } /** + * The toString() method of a buffer reproduces the values in the buffer (as unsigned integers) + * as the character codes of a String. + */ + @Override + public String toString() { + int n = getLen(); + StringBuilder sb = new StringBuilder(n); + for (int i = 0; i < n; i++) { + sb.appendCodePoint(intAt(i)); + } + return sb.toString(); + } + + /** * Check the number of indices (but not their values), raising a Python BufferError if this does * not match the number of dimensions. This is a helper for N-dimensional arrays. * @@ -707,17 +846,25 @@ } /** - * The toString() method of a buffer reproduces the values in the buffer (as unsigned integers) - * as the character codes of a String. + * Check that the buffer is writable. + * + * @throws PyException (TypeError) if not */ - @Override - public String toString() { - int n = getLen(); - StringBuilder sb = new StringBuilder(n); - for (int i = 0; i < n; i++) { - sb.appendCodePoint(intAt(i)); + protected void checkWritable() throws PyException { + if (isReadonly()) { + throw notWritable(); } - return sb.toString(); + } + + /** + * Check that the buffer is backed by an array the client can access as byte[]. + * + * @throws PyException (BufferError) if not + */ + protected void checkHasArray() throws PyException { + if (!hasArray()) { + throw bufferIsNot("accessible as a Java array"); + } } /** diff --git a/src/org/python/core/buffer/BaseNIOBuffer.java b/src/org/python/core/buffer/BaseNIOBuffer.java new file mode 100644 --- /dev/null +++ b/src/org/python/core/buffer/BaseNIOBuffer.java @@ -0,0 +1,249 @@ +package org.python.core.buffer; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ReadOnlyBufferException; + +import org.python.core.PyBUF; +import org.python.core.PyException; + +/** + * Base implementation of the Buffer API for when the storage implementation is + * java.nio.ByteBuffer. The description of {@link BaseBuffer} mostly applies. Methods + * provided or overridden here are appropriate to 1-dimensional arrays, of any item size, backed by + * a ByteBuffer. + */ +public abstract class BaseNIOBuffer extends Base1DBuffer { + + /** + * A {@link java.nio.ByteBuffer} (possibly a direct buffer) wrapping the storage that the + * exporter is sharing with the consumer. The data to be exposed may be only a subset of the + * bytes in the buffer, defined by the navigation information index0, + * shape, strides, etc., usually defined in the constructor. + *

+ * Implementations must not adjust the position and limit of storage after + * construction. It will generally be a duplicate of (not a reference to) a ByteBuffer held by + * the client code. The capacity and backing store are fixed in construction, and the position + * will always be {@link #index0}. The limit is always higher than any valid data, and in the + * case of a contiguous buffer (with positive stride), is exactly just beyond the last item, so + * that a series of ByteBuffer.get operations will yield the data. + */ + protected ByteBuffer storage; + + /** + * Construct an instance of BaseNIOBuffer in support of a sub-class, specifying the + * 'feature flags', or at least a starting set to be adjusted later. Also specify the navigation + * ( {@link #index0}, number of elements, and stride. These 'feature flags' are the features of + * the buffer exported, not the flags that form the consumer's request. The buffer will be + * read-only unless {@link PyBUF#WRITABLE} is set. {@link PyBUF#FORMAT} and + * {@link PyBUF#AS_ARRAY} are implicitly added to the feature flags. + *

+ * To complete initialisation, the sub-class normally must call {@link #checkRequestFlags(int)} + * passing the consumer's request flags. + * + * @param storage the ByteBuffer wrapping the exported object state. NOTE: this + * PyBuffer keeps a reference and may manipulate the position, mark and + * limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy. + * @param featureFlags bit pattern that specifies the features allowed + * @param index0 index into storage of item[0] + * @param size number of elements in the view + * @param stride byte-index step between successive elements + */ + protected BaseNIOBuffer(ByteBuffer storage, int featureFlags, int index0, int size, int stride) { + super(featureFlags & ~(WRITABLE | AS_ARRAY), index0, size, stride); + this.storage = storage; + + // Deduce other feature flags from the client's ByteBuffer + if (!storage.isReadOnly()) { + addFeatureFlags(WRITABLE); + } + if (storage.hasArray()) { + addFeatureFlags(AS_ARRAY); + } + } + + @Override + protected byte byteAtImpl(int byteIndex) throws IndexOutOfBoundsException { + return storage.get(byteIndex); + } + + @Override + protected void storeAtImpl(byte value, int byteIndex) throws PyException { + try { + storage.put(byteIndex, value); + } catch (ReadOnlyBufferException rbe) { + throw notWritable(); + } + } + + @Override + public int byteIndex(int... indices) throws IndexOutOfBoundsException { + // BaseBuffer implementation can be simplified since if indices.length!=1 we error. + checkDimension(indices.length); // throws if != 1 + return byteIndex(indices[0]); + } + + /** + * {@inheritDoc} + *

+ * The default implementation in BaseNIOBuffer deals with the general + * one-dimensional case of arbitrary item size and stride. + */ + @Override + public void copyTo(int srcIndex, byte[] dest, int destPos, int count) + throws IndexOutOfBoundsException { + // Wrap the destination, taking care to reflect the necessary range we shall write. + ByteBuffer destBuf = ByteBuffer.wrap(dest, destPos, count * getItemsize()); + copyTo(srcIndex, destBuf, count); + } + + /** + * Copy all items in this buffer into a ByteBuffer, starting at its current + * position. + * + * @param dest destination buffer + * @throws BufferOverflowException + * @throws ReadOnlyBufferException + */ + // XXX Should this become part of the PyBUffer interface? + public void copyTo(ByteBuffer dest) throws BufferOverflowException, ReadOnlyBufferException { + // Note shape[0] is the number of items in the buffer + copyTo(0, dest, shape[0]); + } + + /** + * Copy a specified number of items from a particular location in this buffer into a + * ByteBuffer, starting at its current position. . + * + * @param srcIndex index of the first item to copy + * @param dest destination buffer + * @param count number of items to copy + * @throws BufferOverflowException + * @throws ReadOnlyBufferException + * @throws IndexOutOfBoundsException + */ + // XXX Should this become part of the PyBuffer interface? + protected void copyTo(int srcIndex, ByteBuffer dest, int count) throws BufferOverflowException, + ReadOnlyBufferException, IndexOutOfBoundsException { + + if (count > 0) { + + ByteBuffer src = getNIOByteBuffer(); + int pos = byteIndex(srcIndex); + + // Pick up attributes necessary to choose an efficient copy strategy + int itemsize = getItemsize(); + int stride = getStrides()[0]; + + // Strategy depends on whether items are laid end-to-end contiguously or there are gaps + if (stride == itemsize) { + // stride == itemsize: straight copy of contiguous bytes + src.limit(pos + count * itemsize).position(pos); + dest.put(src); + + } else if (itemsize == 1) { + // Non-contiguous copy: single byte items + for (int i = 0; i < count; i++) { + src.position(pos); + dest.put(src.get()); + pos += stride; + } + + } else { + // Non-contiguous copy: each time, copy itemsize bytes then skip + for (int i = 0; i < count; i++) { + src.limit(pos + itemsize).position(pos); + dest.put(src); + pos += stride; + } + } + } + } + + /** + * {@inheritDoc} + *

+ * The default implementation in BaseNIOBuffer deals with the general + * one-dimensional case of arbitrary item size and stride. + */ + @Override + public void copyFrom(byte[] src, int srcPos, int destIndex, int count) + throws IndexOutOfBoundsException, PyException { + // Wrap the source, taking care to reflect the range we shall read. + ByteBuffer srcBuf = ByteBuffer.wrap(src, srcPos, count * getItemsize()); + copyFrom(srcBuf, destIndex, count); + } + + /** + * Copy a specified number of items from a ByteBuffer into this buffer at a + * particular location. + * + * @param src source ByteBuffer + * @param destIndex starting item-index in the destination (i.e. this) + * @param count number of items to copy in + * @throws IndexOutOfBoundsException if access out of bounds in source or destination + * @throws PyException (TypeError) if read-only buffer + */ + // XXX Should this become part of the PyBUffer interface? + protected void copyFrom(ByteBuffer src, int destIndex, int count) + throws IndexOutOfBoundsException, PyException { + + checkWritable(); + + if (count > 0) { + + ByteBuffer dest = getNIOByteBuffer(); + int pos = byteIndex(destIndex); + + // Pick up attributes necessary to choose an efficient copy strategy + int itemsize = getItemsize(); + int stride = getStrides()[0]; + int skip = stride - itemsize; + int size = getSize(); + + // Check indexes in destination (this) using the "all non-negative" trick + if ((destIndex | count | size - (destIndex + count)) < 0) { + throw new IndexOutOfBoundsException(); + } + + // Strategy depends on whether items are laid end-to-end or there are gaps + if (skip == 0) { + // Straight copy of contiguous bytes + dest.position(pos); + dest.put(src); + + } else if (itemsize == 1) { + // Non-contiguous copy: single byte items + for (int i = 0; i < count; i++) { + dest.position(pos); + dest.put(src.get()); + // Next byte written will be here + pos += stride; + } + + } else { + // Non-contiguous copy: each time, copy itemsize bytes at a time + for (int i = 0; i < count; i++) { + dest.position(pos); + // Delineate the next itemsize bytes in the src + src.limit(src.position() + itemsize); + dest.put(src); + // Next byte written will be here + pos += stride; + } + } + } + } + + @Override + protected ByteBuffer getNIOByteBufferImpl() { + return storage.duplicate(); + } + + @SuppressWarnings("deprecation") + @Override + public Pointer getBuf() { + checkHasArray(); + return new Pointer(storage.array(), index0); + } +} diff --git a/src/org/python/core/buffer/SimpleBuffer.java b/src/org/python/core/buffer/SimpleBuffer.java --- a/src/org/python/core/buffer/SimpleBuffer.java +++ b/src/org/python/core/buffer/SimpleBuffer.java @@ -1,7 +1,6 @@ package org.python.core.buffer; -import java.nio.ByteBuffer; - +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.PyException; import org.python.core.util.StringUtil; @@ -9,59 +8,31 @@ /** * Buffer API over a read-only one-dimensional array of one-byte items. */ -public class SimpleBuffer extends BaseBuffer { - - /** - * The strides array for this type is always a single element array with a 1 in it. - */ - protected static final int[] SIMPLE_STRIDES = {1}; - - /** - * Provide an instance of SimpleBuffer with navigation variables partly - * initialised, for sub-class use. One-dimensional arrays without strides are C- and - * F-contiguous. To complete initialisation, the sub-class must normally assign the buffer ( - * {@link #storage}, {@link #index0}), and the navigation ({@link #shape} array), and then call - * {@link #checkRequestFlags(int)} passing the consumer's request flags. - */ - protected SimpleBuffer() { - super(CONTIGUITY | SIMPLE); - // Initialise navigation - shape = new int[1]; - strides = SIMPLE_STRIDES; - // suboffsets is always null for this type. - } +public class SimpleBuffer extends BaseArrayBuffer { /** * Provide an instance of SimpleBuffer with navigation variables initialised, for * sub-class use. The buffer ({@link #storage}, {@link #index0}), and the {@link #shape} array - * will be initialised from the arguments (which are checked for range). The {@link #strides} is - * set for (one-byte) unit stride. Only the call to {@link #checkRequestFlags(int)}, passing the - * consumer's request flags really remains for the sub-class constructor to do. + * will be initialised from the arguments (which are not checked for range). The + * {@link #strides} is set for a one-byte stride. Only the call to + * {@link #checkRequestFlags(int)}, passing the consumer's request flags really remains for the + * sub-class constructor to do. * *

      * super(storage, index0, size);
      * checkRequestFlags(flags);        // Check request is compatible with type
      * 
* + * @param obj exporting object (or null) * @param storage the array of bytes storing the implementation of the exporting object * @param index0 offset where the data starts in that array (item[0]) * @param size the number of bytes occupied * @throws NullPointerException if storage is null - * @throws ArrayIndexOutOfBoundsException if index0 and size are - * inconsistent with storage.length */ - // XXX: "for sub-class use" = should be protected? - public SimpleBuffer(byte[] storage, int index0, int size) throws PyException, - ArrayIndexOutOfBoundsException { - this(); - this.storage = storage; // Exported data - this.index0 = index0; // Index to be treated as item[0] - this.shape[0] = size; // Number of items in exported data - - // Check arguments using the "all non-negative" trick - if ((index0 | size | storage.length - (index0 + size)) < 0) { - throw new ArrayIndexOutOfBoundsException(); - } + protected SimpleBuffer(BufferProtocol obj, byte[] storage, int index0, int size) + throws PyException, ArrayIndexOutOfBoundsException { + super(storage, CONTIGUITY | SIMPLE, index0, size, 1); + this.obj = obj; } /** @@ -70,6 +41,7 @@ * against the capabilities of the buffer type. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage the array of bytes storing the implementation of the exporting object * @param index0 offset where the data starts in that array (item[0]) * @param size the number of bytes occupied @@ -78,10 +50,14 @@ * inconsistent with storage.length * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SimpleBuffer(int flags, byte[] storage, int index0, int size) throws PyException, - ArrayIndexOutOfBoundsException, NullPointerException { - this(storage, index0, size); // Construct checked SimpleBuffer - checkRequestFlags(flags); // Check request is compatible with type + public SimpleBuffer(int flags, BufferProtocol obj, byte[] storage, int index0, int size) + throws PyException, ArrayIndexOutOfBoundsException, NullPointerException { + this(obj, storage, index0, size); // Construct checked SimpleBuffer + checkRequestFlags(flags); // Check request is compatible with type + // Check arguments using the "all non-negative" trick + if ((index0 | size | storage.length - (index0 + size)) < 0) { + throw new ArrayIndexOutOfBoundsException(); + } } /** @@ -90,14 +66,12 @@ * {@link #index0}), and the navigation ({@link #shape} array) will be initialised from the * array argument. * + * @param obj exporting object (or null) * @param storage the array of bytes storing the implementation of the exporting object * @throws NullPointerException if storage is null */ - // XXX: "for sub-class use" = should be protected? - public SimpleBuffer(byte[] storage) throws NullPointerException { - this(); - this.storage = storage; // Exported data (index0=0 from initialisation) - this.shape[0] = storage.length; // Number of units in whole array + protected SimpleBuffer(BufferProtocol obj, byte[] storage) throws NullPointerException { + this(obj, storage, 0, storage.length); } /** @@ -106,20 +80,17 @@ * against the capabilities of the buffer type. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage the array of bytes storing the implementation of the exporting object * @throws NullPointerException if storage is null * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SimpleBuffer(int flags, byte[] storage) throws PyException, NullPointerException { - this(storage); // Construct SimpleBuffer on whole array + public SimpleBuffer(int flags, BufferProtocol obj, byte[] storage) throws PyException, + NullPointerException { + this(obj, storage); // Construct SimpleBuffer on whole array checkRequestFlags(flags); // Check request is compatible with type } - @Override - public boolean isReadonly() { - return true; - } - /** * {@inheritDoc} *

@@ -135,72 +106,28 @@ /** * {@inheritDoc} *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. + * In SimpleBuffer the calculation is specialised for one dimension, no striding, + * and an item size of 1. */ @Override - public byte byteAt(int index) throws IndexOutOfBoundsException { - // Implement directly: a bit quicker than the default - return storage[index0 + index]; - } - - /** - * {@inheritDoc} - *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. - */ - @Override - public int intAt(int index) throws IndexOutOfBoundsException { - // Implement directly: a bit quicker than the default - return 0xff & storage[index0 + index]; - } - - @Override - protected int calcIndex(int index) throws IndexOutOfBoundsException { + public int byteIndex(int index) throws IndexOutOfBoundsException { + if (index < 0 || index >= shape[0]) { + throw new IndexOutOfBoundsException(); + } return index0 + index; } - /** - * {@inheritDoc} - *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. - */ + // XXX Consider moving to clauses in getBufferSlice(int, int, int, int) + // to avoid delegation loop where that delegates to this but in BaseBuffer the reverse. @Override - public byte byteAt(int... indices) throws IndexOutOfBoundsException { - checkDimension(indices.length); - return byteAt(indices[0]); - } - - @Override - protected int calcIndex(int... indices) throws IndexOutOfBoundsException { - // BaseBuffer implementation can be simplified since if indices.length!=1 we error. - checkDimension(indices.length); // throws if != 1 - return calcIndex(indices[0]); - } - - /** - * {@inheritDoc} - *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. - */ - @Override - public void copyTo(int srcIndex, byte[] dest, int destPos, int length) - throws IndexOutOfBoundsException { - System.arraycopy(storage, index0 + srcIndex, dest, destPos, length); - } - - @Override - public PyBuffer getBufferSlice(int flags, int start, int length) { - if (length > 0) { + public PyBuffer getBufferSlice(int flags, int start, int count) { + if (count > 0) { // Translate relative to underlying buffer int compIndex0 = index0 + start; // Create the slice from the sub-range of the buffer - return new SimpleView(getRoot(), flags, storage, compIndex0, length); + return new SimpleView(getRoot(), flags, storage, compIndex0, count); } else { - // Special case for length==0 where above logic would fail. Efficient too. + // Special case for count==0 where above logic would fail. Efficient too. return new ZeroByteBuffer.View(getRoot(), flags); } } @@ -210,38 +137,33 @@ *

* SimpleBuffer provides an implementation for slicing contiguous bytes in one * dimension. In that case, x(i) = u(r+i) for i = 0..L-1 where u is the underlying - * buffer, and r and L are the start and length with which x was created + * buffer, and r and L are the start and count with which x was created * from u. Thus y(k) = u(r+s+km), that is, the composite offset is r+s and * the stride is m. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { - if (stride == 1 || length < 2) { + if (stride == 1 || count < 2) { // Unstrided slice of simple buffer is itself simple - return getBufferSlice(flags, start, length); + return getBufferSlice(flags, start, count); } else { // Translate relative to underlying buffer int compIndex0 = index0 + start; // Construct a view, taking a lock on the root object (this or this.root) - return new Strided1DBuffer.SlicedView(getRoot(), flags, storage, compIndex0, length, + return new Strided1DBuffer.SlicedView(getRoot(), flags, storage, compIndex0, count, stride); } } - @Override - public ByteBuffer getNIOByteBuffer() { - // Simplify for one-dimensional contiguous bytes - ByteBuffer b = ByteBuffer.wrap(storage, index0, shape[0]); - return isReadonly() ? b.asReadOnlyBuffer() : b; - } - + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int index) throws IndexOutOfBoundsException { return new Pointer(storage, index0 + index); } + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int... indices) throws IndexOutOfBoundsException { checkDimension(indices.length); @@ -274,7 +196,7 @@ */ public SimpleView(PyBuffer root, int flags, byte[] storage, int offset, int size) { // Create a new SimpleBuffer on the buffer passed in (part of the root) - super(flags, storage, offset, size); + super(flags, root.getObj(), storage, offset, size); // Get a lease on the root PyBuffer this.root = root.getBuffer(FULL_RO); } @@ -283,13 +205,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } - } - } diff --git a/src/org/python/core/buffer/SimpleNIOBuffer.java b/src/org/python/core/buffer/SimpleNIOBuffer.java new file mode 100644 --- /dev/null +++ b/src/org/python/core/buffer/SimpleNIOBuffer.java @@ -0,0 +1,190 @@ +package org.python.core.buffer; + +import java.nio.ByteBuffer; + +import org.python.core.BufferProtocol; +import org.python.core.PyBuffer; +import org.python.core.PyException; + +/** + * Buffer API over a read-only one-dimensional java.nio.ByteBuffer of one-byte items. + */ +public class SimpleNIOBuffer extends BaseNIOBuffer { + + /** + * Provide an instance of SimpleNIOBuffer with navigation variables initialised, + * for sub-class use. The buffer ({@link #storage}, {@link #index0}), and the {@link #shape} + * array will be initialised from the arguments (which are checked for range). The + * {@link #strides} is set for (one-byte) unit stride. Only the call to + * {@link #checkRequestFlags(int)}, passing the consumer's request flags, really remains for the + * sub-class constructor to do. + * + *

+     * super(storage.duplicate(), index0, size);
+     * checkRequestFlags(flags);        // Check request is compatible with type
+     * 
+ * + * @param obj exporting object (or null) + * @param storage the ByteBuffer wrapping the exported object state. NOTE: this + * PyBuffer keeps a reference and may manipulate the position, mark and + * limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy. + * @param index0 offset where the data starts in that array (item[0]) + * @param size the number of bytes occupied + * @throws NullPointerException if storage is null + * @throws ArrayIndexOutOfBoundsException if index0 and size are + * inconsistent with storage.capacity() + */ + protected SimpleNIOBuffer(BufferProtocol obj, ByteBuffer storage, int index0, int size) + throws PyException, ArrayIndexOutOfBoundsException { + super(storage, CONTIGUITY | SIMPLE, index0, size, 1); + this.obj = obj; + // Check arguments using the "all non-negative" trick + if ((index0 | size | storage.capacity() - (index0 + size)) < 0) { + throw new ArrayIndexOutOfBoundsException(); + } + } + + /** + * Provide an instance of SimpleNIOBuffer, on a slice of a {@link ByteBuffer}, + * meeting the consumer's expectations as expressed in the flags argument, which is + * checked against the capabilities of the buffer type. No reference will be kept to the + * ByteBuffer passed in. (It is duplicated.) + * + * @param flags consumer requirements + * @param obj exporting object (or null) + * @param storage the ByteBuffer wrapping the exported object state + * @param index0 offset where the data starts in that buffer (item[0]) + * @param size the number of bytes occupied + * @throws NullPointerException if storage is null + * @throws ArrayIndexOutOfBoundsException if index0 and size are + * inconsistent with storage.length + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public SimpleNIOBuffer(int flags, BufferProtocol obj, ByteBuffer storage, int index0, int size) + throws PyException, ArrayIndexOutOfBoundsException, NullPointerException { + this(obj, storage.duplicate(), index0, size); // Construct checked SimpleNIOBuffer + checkRequestFlags(flags); // Check request is compatible with type + } + + /** + * Provide an instance of SimpleNIOBuffer, on the entirety of a {@link ByteBuffer}, + * with navigation variables initialised, for sub-class use. The buffer ( {@link #storage}, + * {@link #index0}), and the navigation ({@link #shape} array) will be initialised from the + * argument. + * + * @param obj exporting object (or null) + * @param storage the ByteBuffer wrapping the exported object state. NOTE: this + * PyBuffer keeps a reference and may manipulate the position, mark and + * limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy. + * @throws NullPointerException if storage is null + */ + protected SimpleNIOBuffer(BufferProtocol obj, ByteBuffer storage) throws NullPointerException { + this(obj, storage, 0, storage.capacity()); + } + + /** + * Provide an instance of SimpleNIOBuffer, on the entirety of a {@link ByteBuffer}, + * meeting the consumer's expectations as expressed in the flags argument, which is + * checked against the capabilities of the buffer type. No reference will be kept to the + * ByteBuffer passed in. (It is duplicated.) + * + * @param flags consumer requirements + * @param obj exporting object (or null) + * @param storage the ByteBuffer wrapping the exported object state + * @throws NullPointerException if storage is null + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public SimpleNIOBuffer(int flags, BufferProtocol obj, ByteBuffer storage) throws PyException, + NullPointerException { + this(obj, storage.duplicate()); // Construct SimpleNIOBuffer on whole ByteBuffer + checkRequestFlags(flags); // Check request is compatible with type + } + + /** + * {@inheritDoc} + *

+ * SimpleNIOBuffer provides an implementation optimised for contiguous bytes in + * one-dimension. + */ + @Override + public int getLen() { + // Simplify for one-dimensional contiguous bytes + return shape[0]; + } + + @Override + public final int byteIndex(int index) throws IndexOutOfBoundsException { + return index0 + index; + } + + // XXX Consider moving to clauses in getBufferSlice(int, int, int, int) + // to avoid delegation loop where that delegates to this but in BaseBuffer the reverse. + @Override + public PyBuffer getBufferSlice(int flags, int start, int count) { + if (count > 0) { + // Translate relative to underlying buffer + int compIndex0 = index0 + start; + // Create the slice from the sub-range of the buffer + return new SimpleView(getRoot(), flags, storage, compIndex0, count); + } else { + // Special case for count==0 where above logic would fail. Efficient too. + return new ZeroByteBuffer.View(getRoot(), flags); + } + } + + /** + * {@inheritDoc} + *

+ * SimpleNIOBuffer provides an implementation for slicing contiguous bytes in one + * dimension. In that case, x(i) = u(r+i) for i = 0..L-1 where u is the underlying + * buffer, and r and L are the start and count with which x was created + * from u. Thus y(k) = u(r+s+km), that is, the composite offset is r+s and + * the stride is m. + */ + @Override + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { + + if (stride == 1 || count < 2) { + // Unstrided slice of simple buffer is special case + return getBufferSlice(flags, start, count); + + } else { + // Translate relative to underlying buffer + int compIndex0 = index0 + start; + // Construct a view, taking a lock on the root object (this or this.root) + return new Strided1DNIOBuffer.SlicedView(getRoot(), flags, storage, compIndex0, count, + stride); + } + } + + /** + * A SimpleNIOBuffer.SimpleView represents a contiguous subsequence of another + * SimpleNIOBuffer. + */ + static class SimpleView extends SimpleNIOBuffer { + + /** The buffer on which this is a slice view */ + PyBuffer root; + + /** + * Construct a slice of a SimpleNIOBuffer. + * + * @param root buffer which will be acquired and must be released ultimately + * @param flags the request flags of the consumer that requested the slice + * @param storage ByteBuffer wrapping exported data (no reference kept) + * @param offset where the data starts in that buffer (item[0]) + * @param count the number of items in the sliced view + */ + public SimpleView(PyBuffer root, int flags, ByteBuffer storage, int offset, int count) { + // Create a new SimpleNIOBuffer on the buffer passed in (part of the root) + super(flags, root.getObj(), storage, offset, count); + // Get a lease on the root PyBuffer + this.root = root.getBuffer(FULL_RO); + } + + @Override + protected PyBuffer getRoot() { + return root; + } + } +} diff --git a/src/org/python/core/buffer/SimpleStringBuffer.java b/src/org/python/core/buffer/SimpleStringBuffer.java --- a/src/org/python/core/buffer/SimpleStringBuffer.java +++ b/src/org/python/core/buffer/SimpleStringBuffer.java @@ -2,17 +2,18 @@ import java.nio.ByteBuffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.util.StringUtil; /** * Buffer API that appears to be a one-dimensional array of one-byte items providing read-only API, * but which is actually backed by a Java String. Some of the buffer API absolutely needs access to - * the data as a byte array (those parts that involve a PyBuffer.Pointer result), and - * therefore this class must create a byte array from the String for them. However, it defers - * creation of a byte array until that part of the API is actually used. Where possible, this class - * overrides those methods in SimpleBuffer that would otherwise access the byte array attribute to - * use the String instead. + * the data as a byte array (those parts that involve a {@link java.nio.ByteBuffer} or + * {@link PyBuffer.Pointer} result), and therefore this class must create a byte array from the + * String for them. However, it defers creation of a byte array until that part of the API is + * actually used. Where possible, this class overrides those methods in SimpleBuffer that would + * otherwise access the byte array attribute to use the String instead. */ public class SimpleStringBuffer extends SimpleBuffer { @@ -26,13 +27,18 @@ * Provide an instance of SimpleStringBuffer meeting the consumer's expectations as expressed in * the flags argument. * + * @param flags consumer requirements + * @param obj exporting object (or null) * @param bufString storing the implementation of the object - * @param flags consumer requirements */ - public SimpleStringBuffer(int flags, String bufString) { + public SimpleStringBuffer(int flags, BufferProtocol obj, String bufString) { + /* + * Leaving storage=null is ok because we carefully override every method that uses it, + * deferring creation of the storage byte array until we absolutely must have one. + */ + super(obj, null, 0, bufString.length()); // Save the backing string this.bufString = bufString; - shape[0] = bufString.length(); // Check request is compatible with type checkRequestFlags(flags); } @@ -54,32 +60,31 @@ * This method uses {@link String#charAt(int)} rather than create an actual byte buffer. */ @Override - public byte byteAt(int index) throws IndexOutOfBoundsException { - // Avoid creating buf by using String.charAt + public final byte byteAtImpl(int index) { return (byte)bufString.charAt(index); } /** * {@inheritDoc} *

+ * In SimpleStringBuffer we can simply return the argument. + */ + @Override + public final int byteIndex(int index) { + // We do not check the index because String will do it for us. + return index; + } + + /** + * {@inheritDoc} + *

* This method uses {@link String#charAt(int)} rather than create an actual byte buffer. */ @Override - public int intAt(int index) throws IndexOutOfBoundsException { - // Avoid creating buf by using String.charAt - return bufString.charAt(index); - } - - /** - * {@inheritDoc} - *

- * This method uses {@link String#charAt(int)} rather than create an actual byte buffer. - */ - @Override - public void copyTo(int srcIndex, byte[] dest, int destPos, int length) + public void copyTo(int srcIndex, byte[] dest, int destPos, int count) throws IndexOutOfBoundsException { // Avoid creating buf by using String.charAt - int endIndex = srcIndex + length, p = destPos; + int endIndex = srcIndex + count, p = destPos; for (int i = srcIndex; i < endIndex; i++) { dest[p++] = (byte)bufString.charAt(i); } @@ -91,13 +96,12 @@ * The SimpleStringBuffer implementation avoids creation of a byte buffer. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length) { - if (length > 0) { - // The new string content is just a sub-string. (Non-copy operation in Java.) - return new SimpleStringView(getRoot(), flags, - bufString.substring(start, start + length)); + public PyBuffer getBufferSlice(int flags, int start, int count) { + if (count > 0) { + // The new string content is just a sub-string. + return new SimpleStringView(getRoot(), flags, bufString.substring(start, start + count)); } else { - // Special case for length==0 where start out of bounds sometimes raises exception. + // Special case for count==0 where start out of bounds sometimes raises exception. return new ZeroByteBuffer.View(getRoot(), flags); } } @@ -108,23 +112,26 @@ * The SimpleStringBuffer implementation creates an actual byte buffer. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { if (stride == 1) { // Unstrided slice of a SimpleStringBuffer is itself a SimpleStringBuffer. - return getBufferSlice(flags, start, length); + return getBufferSlice(flags, start, count); } else { // Force creation of the actual byte array from the String. ensureHaveBytes(); // Now we are effectively a SimpleBuffer, return the strided view. - return super.getBufferSlice(flags, start, length, stride); + return super.getBufferSlice(flags, start, count, stride); } } @Override - public ByteBuffer getNIOByteBuffer() { + protected ByteBuffer getNIOByteBufferImpl() { // Force creation of the actual byte array from the String. ensureHaveBytes(); - return super.getNIOByteBuffer().asReadOnlyBuffer(); + // The buffer spans the whole storage, which may include data not in the view + ByteBuffer b = ByteBuffer.wrap(storage); + // Return as read-only. + return b.asReadOnlyBuffer(); } /** @@ -142,6 +149,7 @@ *

* This method creates an actual byte array from the underlying String if none yet exists. */ + @SuppressWarnings("deprecation") @Override public Pointer getBuf() { ensureHaveBytes(); @@ -153,6 +161,7 @@ *

* This method creates an actual byte array from the underlying String if none yet exists. */ + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int index) { ensureHaveBytes(); @@ -164,6 +173,7 @@ *

* This method creates an actual byte array from the underlying String if none yet exists. */ + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int... indices) { ensureHaveBytes(); @@ -197,7 +207,7 @@ */ public SimpleStringView(PyBuffer root, int flags, String bufString) { // Create a new SimpleStringBuffer on the string passed in - super(flags, bufString); + super(flags, root.getObj(), bufString); // Get a lease on the root PyBuffer this.root = root.getBuffer(FULL_RO); } @@ -206,12 +216,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } - } } diff --git a/src/org/python/core/buffer/SimpleWritableBuffer.java b/src/org/python/core/buffer/SimpleWritableBuffer.java --- a/src/org/python/core/buffer/SimpleWritableBuffer.java +++ b/src/org/python/core/buffer/SimpleWritableBuffer.java @@ -1,5 +1,6 @@ package org.python.core.buffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.PyException; @@ -14,16 +15,17 @@ * against the capabilities of the buffer type. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage the array of bytes storing the implementation of the exporting object * @param index0 offset where the data starts in that array (item[0]) * @param size the number of bytes occupied * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SimpleWritableBuffer(int flags, byte[] storage, int index0, int size) + public SimpleWritableBuffer(int flags, BufferProtocol obj, byte[] storage, int index0, int size) throws PyException, NullPointerException { - super(storage, index0, size); // Construct checked SimpleBuffer + super(obj, storage, index0, size); // Construct checked SimpleBuffer addFeatureFlags(WRITABLE); - checkRequestFlags(flags); // Check request is compatible with type + checkRequestFlags(flags); // Check request is compatible with type } /** @@ -32,68 +34,34 @@ * checked against the capabilities of the buffer type. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage the array of bytes storing the implementation of the exporting object * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SimpleWritableBuffer(int flags, byte[] storage) throws PyException, NullPointerException { - super(storage); // Construct SimpleBuffer on whole array - addFeatureFlags(WRITABLE); - checkRequestFlags(flags); // Check request is compatible with type - } - - @Override - public boolean isReadonly() { - return false; + public SimpleWritableBuffer(int flags, BufferProtocol obj, byte[] storage) throws PyException, + NullPointerException { + this(flags, obj, storage, 0, storage.length); } /** * {@inheritDoc} *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. + * Declared final returning true in SimpleWritableBuffer + * to make checks unnecessary. */ @Override - public void storeAt(byte value, int index) { - // Implement directly and don't ask whether read-only - storage[index0 + index] = value; + public final boolean isReadonly() { + return false; } - /** - * {@inheritDoc} - *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. - */ + /** Do nothing: the buffer is writable. */ @Override - public void storeAt(byte value, int... indices) { - checkDimension(indices.length); - storeAt(value, indices[0]); - } + protected final void checkWritable() {} - /** - * {@inheritDoc} - *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. - */ @Override - public void copyFrom(byte[] src, int srcPos, int destIndex, int length) { - System.arraycopy(src, srcPos, storage, index0 + destIndex, length); - } - - /** - * {@inheritDoc} - *

- * SimpleBuffer provides an implementation optimised for contiguous bytes in - * one-dimension. - */ - @Override - public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { - if (src.getLen() != getLen()) { - throw differentStructure(); - } - // Get the source to deliver efficiently to our byte storage - src.copyTo(storage, index0); + protected void storeAtImpl(byte value, int byteIndex) { + // Implement directly and don't ask whether read-only + storage[byteIndex] = value; } /** @@ -103,14 +71,14 @@ * writable. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length) { - if (length > 0) { + public PyBuffer getBufferSlice(int flags, int start, int count) { + if (count > 0) { // Translate relative to underlying buffer int compIndex0 = index0 + start; // Create the slice from the sub-range of the buffer - return new SimpleView(getRoot(), flags, storage, compIndex0, length); + return new SimpleView(getRoot(), flags, storage, compIndex0, count); } else { - // Special case for length==0 where above logic would fail. Efficient too. + // Special case for count==0 where above logic would fail. Efficient too. return new ZeroByteBuffer.View(getRoot(), flags); } } @@ -122,18 +90,18 @@ * writable. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { - if (stride == 1 || length < 2) { + if (stride == 1 || count < 2) { // Unstrided slice of simple buffer is itself simple - return getBufferSlice(flags, start, length); + return getBufferSlice(flags, start, count); } else { // Translate relative to underlying buffer int compIndex0 = index0 + start; // Construct a view, taking a lock on the root object (this or this.root) return new Strided1DWritableBuffer.SlicedView(getRoot(), flags, storage, compIndex0, - length, stride); + count, stride); } } @@ -157,7 +125,7 @@ */ public SimpleView(PyBuffer root, int flags, byte[] storage, int index0, int size) { // Create a new SimpleBuffer on the buffer passed in (part of the root) - super(flags, storage, index0, size); + super(flags, root.getObj(), storage, index0, size); // Get a lease on the root PyBuffer this.root = root.getBuffer(FULL_RO); } @@ -166,13 +134,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } - } - } diff --git a/src/org/python/core/buffer/Strided1DBuffer.java b/src/org/python/core/buffer/Strided1DBuffer.java --- a/src/org/python/core/buffer/Strided1DBuffer.java +++ b/src/org/python/core/buffer/Strided1DBuffer.java @@ -1,11 +1,12 @@ package org.python.core.buffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.PyException; /** * Read-only buffer API over a one-dimensional array of one-byte items, that are evenly-spaced in a - * storage array. The buffer has storage, index0 and length + * storage array. The buffer has storage, index0 and count * properties in the usual way, designating a slice (or all) of a byte array, but also a * stride property (equal to getStrides()[0]). *

@@ -14,7 +15,7 @@ * Designate by x(j), for j=0..L-1, the byte at index j, that is, the byte * retrieved by x.byteAt(j). Thus, we store x(j) at u(a+pj), that is, * x(0) = u(a). When we construct such a buffer, we have to supply a = - * index0, L = length, and p = stride as the + * index0, L = count, and p = stride as the * constructor arguments. The last item in the slice x(L-1) is stored at u(a+p(L-1)). * For the simple case of positive stride, constructor argument index0 is the low index * of the range occupied by the data. When the stride is negative, that is to say p<0, and @@ -28,7 +29,7 @@ * create the memoryview that is returned as an extended slice of a * memoryview. */ -public class Strided1DBuffer extends BaseBuffer { +public class Strided1DBuffer extends BaseArrayBuffer { /** * Step size in the underlying buffer essential to correct translation of an index (or indices) @@ -38,30 +39,6 @@ protected int stride; /** - * Provide an instance of Strided1DBuffer with navigation variables partly - * initialised, for sub-class use. To complete initialisation, the sub-class normally must - * assign the navigational properties and call {@link #checkRequestFlags(int)} passing the - * consumer's request flags. - * - *

-     * this.storage = storage;          // Exported data
-     * this.index0 = index0;            // Index to be treated as item[0]
-     * this.shape[0] = length;          // Number of items in exported data
-     * this.stride = stride;            // Between items
-     * 
- * - * The pre-defined {@link #strides} field remains null until {@link #getStrides} is - * called. - */ - protected Strided1DBuffer() { - super(STRIDES); - // Initialise navigation - shape = new int[1]; - // strides is created on demand; - // suboffsets is always null for this type. - } - - /** * Provide an instance of Strided1DBuffer with navigation variables initialised, * for sub-class use. The buffer ({@link #storage}, {@link #index0}), and the navigation ( * {@link #shape} array and {@link #stride}) will be initialised from the arguments (which are @@ -69,27 +46,25 @@ *

* The sub-class constructor should check that the intended access is compatible with this * object by calling {@link #checkRequestFlags(int)}. (See the source of - * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, byte[], int, int, int)} - * for an example of this use.) + * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, byte[], int, int, int)} for an + * example of this use.) * + * @param obj exporting object (or null) * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] - * @param length number of items in the slice + * @param count number of items in the slice * @param stride in between successive elements of the new PyBuffer * @throws NullPointerException if storage is null - * @throws ArrayIndexOutOfBoundsException if index0, length and + * @throws ArrayIndexOutOfBoundsException if index0, count and * stride are inconsistent with storage.length */ - // XXX: "for sub-class use" = should be protected? - public Strided1DBuffer(byte[] storage, int index0, int length, int stride) + protected Strided1DBuffer(BufferProtocol obj, byte[] storage, int index0, int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException { - this(); - this.storage = storage; // Exported data - this.index0 = index0; // Index to be treated as item[0] - this.shape[0] = length; // Number of items in exported data + super(storage, STRIDES, index0, count, stride); + this.obj = obj; this.stride = stride; // Between items - if (length == 0) { + if (count == 0) { // Nothing to check as we'll make no accesses addFeatureFlags(CONTIGUITY); @@ -99,20 +74,20 @@ if (stride == 1) { lo = index0; // First byte of item[0] - hi = index0 + length; // Last byte of item[L-1] + 1 + hi = index0 + count; // Last byte of item[L-1] + 1 addFeatureFlags(CONTIGUITY); } else if (stride > 1) { lo = index0; // First byte of item[0] - hi = index0 + (length - 1) * stride + 1; // Last byte of item[L-1] + 1 + hi = index0 + (count - 1) * stride + 1; // Last byte of item[L-1] + 1 } else { hi = index0 + 1; // Last byte of item[0] + 1 - lo = index0 + (length - 1) * stride; // First byte of item[L-1] + lo = index0 + (count - 1) * stride; // First byte of item[L-1] } // Check indices using "all non-negative" trick - if ((length | lo | (storage.length - lo) | hi | (storage.length - hi)) < 0) { + if ((count | lo | (storage.length - lo) | hi | (storage.length - hi)) < 0) { throw new ArrayIndexOutOfBoundsException(); } } @@ -122,31 +97,32 @@ * Provide an instance of Strided1DBuffer on a particular array of bytes specifying * a starting index, the number of items in the result, and a byte-indexing stride. The result * of byteAt(i) will be equal to storage[index0+stride*i] (whatever - * the sign of stride), valid for 0<=i<length. The constructor + * the sign of stride), valid for 0<=i<count. The constructor * checks that all these indices lie within the storage array (unless - * length=0). + * count=0). *

* The constructed PyBuffer meets the consumer's expectations as expressed in the * flags argument, or an exception will be thrown if these are incompatible with * the type (e.g. the consumer does not specify that it understands the strides array). Note * that the actual range in the storage array, the lowest and highest index, is not - * explicitly passed, but is implicit in index0, length and + * explicitly passed, but is implicit in index0, count and * stride. The constructor checks that these indices lie within the - * storage array (unless length=0). + * storage array (unless count=0). * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] - * @param length number of items in the slice - * @param stride in between successive elements of the new PyBuffer + * @param count number of items in the slice + * @param stride byte-index distance from one element to the next in the new PyBuffer * @throws NullPointerException if storage is null - * @throws ArrayIndexOutOfBoundsException if index0, length and + * @throws ArrayIndexOutOfBoundsException if index0, count and * stride are inconsistent with storage.length * @throws PyException (BufferError) when expectations do not correspond with the type */ - public Strided1DBuffer(int flags, byte[] storage, int index0, int length, int stride) + public Strided1DBuffer(int flags, BufferProtocol obj, byte[] storage, int index0, int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException, PyException { - this(storage, index0, length, stride); + this(obj, storage, index0, count, stride); checkRequestFlags(flags); // Check request is compatible with type } @@ -157,78 +133,46 @@ } @Override - public byte byteAt(int index) throws IndexOutOfBoundsException { - return storage[index0 + index * stride]; - } - - @Override - protected int calcIndex(int index) throws IndexOutOfBoundsException { + public final int byteIndex(int index) throws IndexOutOfBoundsException { + if (index < 0 || index >= shape[0]) { + throw new IndexOutOfBoundsException(); + } return index0 + index * stride; } - @Override - protected int calcIndex(int... indices) throws IndexOutOfBoundsException { - // BaseBuffer implementation can be simplified since if indices.length!=1 we error. - checkDimension(indices.length); // throws if != 1 - return calcIndex(indices[0]); - } - - /** - * {@inheritDoc} Strided1DBuffer provides a version optimised for strided bytes in - * one dimension. - */ - @Override - public void copyTo(int srcIndex, byte[] dest, int destPos, int length) - throws IndexOutOfBoundsException { - // Data is here in the buffers - int s = index0 + srcIndex * stride; - int d = destPos; - - // Strategy depends on whether items are laid end-to-end contiguously or there are gaps - if (stride == 1) { - // stride == itemsize: straight copy of contiguous bytes - System.arraycopy(storage, s, dest, d, length); - - } else { - // Non-contiguous copy: single byte items - int limit = s + length * stride; - for (; s != limit; s += stride) { - dest[d++] = storage[s]; - } - } - } - /** * {@inheritDoc} *

* Strided1DBuffer provides an implementation for slicing already-strided bytes in * one dimension. In that case, x(i) = u(r+ip) for i = 0..L-1 where u is the - * underlying buffer, and r, p and L are the start, stride and length with + * underlying buffer, and r, p and L are the start, stride and count with * which x was created from u. Thus y(k) = u(r+sp+kmp), that is, the * composite index0 is r+sp and the composite stride is * mp. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { - if (length > 0) { + if (count > 0) { // Translate start relative to underlying buffer int compStride = this.stride * stride; int compIndex0 = index0 + start * this.stride; // Construct a view, taking a lock on the root object (this or this.root) - return new SlicedView(getRoot(), flags, storage, compIndex0, length, compStride); + return new SlicedView(getRoot(), flags, storage, compIndex0, count, compStride); } else { - // Special case for length==0 where above logic would fail. Efficient too. + // Special case for count==0 where above logic would fail. Efficient too. return new ZeroByteBuffer.View(getRoot(), flags); } } + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int index) { return new Pointer(storage, index0 + index * stride); } + @SuppressWarnings("deprecation") @Override public Pointer getPointer(int... indices) { // BaseBuffer implementation can be simplified since if indices.length!=1 we error. @@ -236,15 +180,6 @@ return getPointer(indices[0]); } - @Override - public int[] getStrides() { - if (strides == null) { - strides = new int[1]; - strides[0] = stride; - } - return strides; - } - /** * A Strided1DBuffer.SlicedView represents a non-contiguous subsequence of a simple * buffer. @@ -261,14 +196,14 @@ * @param flags consumer requirements * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] - * @param len number of items in the slice + * @param count number of items in the sliced view * @param stride in between successive elements of the new PyBuffer * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int len, int stride) - throws PyException { + public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int count, + int stride) throws PyException { // Create a new on the buffer passed in (part of the root) - super(flags, storage, index0, len, stride); + super(flags, root.getObj(), storage, index0, count, stride); // Get a lease on the root PyBuffer (read-only) this.root = root.getBuffer(FULL_RO); } @@ -277,13 +212,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } - } - } diff --git a/src/org/python/core/buffer/Strided1DNIOBuffer.java b/src/org/python/core/buffer/Strided1DNIOBuffer.java new file mode 100644 --- /dev/null +++ b/src/org/python/core/buffer/Strided1DNIOBuffer.java @@ -0,0 +1,209 @@ +package org.python.core.buffer; + +import java.nio.ByteBuffer; + +import org.python.core.BufferProtocol; +import org.python.core.PyBuffer; +import org.python.core.PyException; + +/** + * Read-only buffer API over a one-dimensional java.nio.ByteBuffer of one-byte items, + * that are evenly-spaced in that store. The buffer has storage, index0 + * and length properties in the usual way, designating a slice (or all) of a byte + * array, but also a stride property (equal to getStrides()[0]). + *

+ * Let the underlying buffer be the byte sequence u(i) for i=0..N-1, let x be + * the Strided1DNIOBuffer, and let the stride be p. The storage works as + * follows. Designate by x(j), for j=0..L-1, the byte at index j, that is, the + * byte retrieved by x.byteAt(j). Thus, we store x(j) at u(a+pj), that + * is, x(0) = u(a). When we construct such a buffer, we have to supply a = + * index0, L = count, and p = stride as the + * constructor arguments. The last item in the slice x(L-1) is stored at u(a+p(L-1)). + * For the simple case of positive stride, constructor argument index0 is the low index + * of the range occupied by the data. When the stride is negative, that is to say p<0, and + * L>1, this will be to the left of u(a), and the constructor argument + * index0 is not then the low index of the range occupied by the data. Clearly both + * these indexes must be in the range 0 to N-1 inclusive, a rule enforced by the constructors + * (unless L=0, when it is assumed no array access will take place). + *

+ * The class may be used by exporters to create a strided slice (e.g. to export the diagonal of a + * matrix) and in particular by other buffers to create strided slices of themselves, such as to + * create the memoryview that is returned as an extended slice of a + * memoryview. + */ +public class Strided1DNIOBuffer extends BaseNIOBuffer { + + /** + * Step size in the underlying buffer essential to correct translation of an index (or indices) + * into an index into the storage. The value is returned by {@link #getStrides()} is an array + * with this as the only element. + */ + protected int stride; + + /** + * Provide an instance of Strided1DNIOBuffer with navigation variables initialised, + * for sub-class use. The buffer ({@link #storage}, {@link #index0}), and the navigation ( + * {@link #shape} array and {@link #stride}) will be initialised from the arguments (which are + * checked for range). + *

+ * The sub-class constructor should check that the intended access is compatible with this + * object by calling {@link #checkRequestFlags(int)}. (See the source of + * {@link Strided1DWritableBuffer#Strided1DWritableBuffer(int, ByteBuffer, int, int, int)} for + * an example of this use.) + * + * @param obj exporting object (or null) + * @param storage the ByteBuffer wrapping the exported object state. NOTE: this + * PyBuffer keeps a reference and may manipulate the position, mark and + * limit hereafter. Use {@link ByteBuffer#duplicate()} to give it an isolated copy. + * @param index0 index into storage of item[0] + * @param count number of items in the slice + * @param stride in between successive elements of the new PyBuffer + * @throws NullPointerException if storage is null + * @throws ArrayIndexOutOfBoundsException if index0, count and + * stride are inconsistent with storage.length + */ + protected Strided1DNIOBuffer(BufferProtocol obj, ByteBuffer storage, int index0, int count, + int stride) throws ArrayIndexOutOfBoundsException, NullPointerException { + super(storage, STRIDES, index0, count, stride); + this.obj = obj; + this.stride = stride; // Between items + + if (count == 0) { + // Nothing to check as we'll make no accesses + addFeatureFlags(CONTIGUITY); + + } else { + // Need to check lowest and highest index against array + int lo, hi; + + if (stride == 1) { + lo = index0; // First byte of item[0] + hi = index0 + count; // Last byte of item[L-1] + 1 + addFeatureFlags(CONTIGUITY); + + } else if (stride > 1) { + lo = index0; // First byte of item[0] + hi = index0 + (count - 1) * stride + 1; // Last byte of item[L-1] + 1 + + } else { + hi = index0 + 1; // Last byte of item[0] + 1 + lo = index0 + (count - 1) * stride; // First byte of item[L-1] + } + + // Check indices using "all non-negative" trick + int cap = storage.capacity(); + if ((count | lo | (cap - lo) | hi | (cap - hi)) < 0) { + throw new ArrayIndexOutOfBoundsException(); + } + } + + // Deduce feature flags from the client's ByteBuffer + if (!storage.isReadOnly()) { + addFeatureFlags(WRITABLE); + } + if (storage.hasArray()) { + addFeatureFlags(AS_ARRAY); + } + } + + /** + * Provide an instance of Strided1DNIOBuffer on a particular {@link ByteBuffer} + * specifying a starting index, the number of items in the result, and a byte-indexing stride. + * The result of byteAt(i) will be equal to + * storage.get(index0+stride*i) (whatever the sign of stride), valid + * for 0<=i<count. The constructor checks that all these indices lie within + * the storage (unless count=0). No reference will be kept to the + * ByteBuffer passed in. (It is duplicated.) + *

+ * The constructed PyBuffer meets the consumer's expectations as expressed in the + * flags argument, or an exception will be thrown if these are incompatible with + * the type (e.g. the consumer does not specify that it understands the strides array). Note + * that the actual range in the storage array, the lowest and highest index, is not + * explicitly passed, but is implicit in index0, count and + * stride. The constructor checks that these indices lie within the + * storage array (unless count=0). + * + * @param flags consumer requirements + * @param obj exporting object (or null) + * @param storage ByteBuffer wrapping exported data + * @param index0 index into storage of item[0] + * @param count number of items in the slice + * @param stride in between successive elements of the new PyBuffer + * @throws NullPointerException if storage is null + * @throws ArrayIndexOutOfBoundsException if index0, count and + * stride are inconsistent with storage.length + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public Strided1DNIOBuffer(int flags, BufferProtocol obj, ByteBuffer storage, int index0, + int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException, + PyException { + this(obj, storage.duplicate(), index0, count, stride); + checkRequestFlags(flags); // Check request is compatible with type + + } + + @Override + public final int byteIndex(int index) throws IndexOutOfBoundsException { + return index0 + index * stride; + } + + /** + * {@inheritDoc} + *

+ * Strided1DNIOBuffer provides an implementation for slicing already-strided bytes + * in one dimension. In that case, x(i) = u(r+ip) for i = 0..L-1 where u is the + * underlying buffer, and r, p and L are the start, stride and count with + * which x was created from u. Thus y(k) = u(r+sp+kmp), that is, the + * composite index0 is r+sp and the composite stride is + * mp. + */ + @Override + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { + + if (count > 0) { + // Translate start relative to underlying buffer + int compStride = this.stride * stride; + int compIndex0 = index0 + start * this.stride; + // Construct a view, taking a lock on the root object (this or this.root) + return new SlicedView(getRoot(), flags, storage, compIndex0, count, compStride); + + } else { + // Special case for count==0 where above logic would fail. Efficient too. + return new ZeroByteBuffer.View(getRoot(), flags); + } + } + + /** + * A Strided1DNIOBuffer.SlicedView represents a non-contiguous subsequence of a + * simple buffer. + */ + static class SlicedView extends Strided1DNIOBuffer { + + /** The buffer on which this is a slice view */ + PyBuffer root; + + /** + * Construct a slice of a one-dimensional byte buffer. + * + * @param root on which release must be called when this is released + * @param flags consumer requirements + * @param storage ByteBuffer wrapping exported data (no reference kept) + * @param index0 index into storage of item[0] + * @param count the number of items in the sliced view + * @param stride in between successive elements of the new PyBuffer + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public SlicedView(PyBuffer root, int flags, ByteBuffer storage, int index0, int count, + int stride) throws PyException { + // Create a new slice on the buffer passed in (part of the root) + super(flags, root.getObj(), storage, index0, count, stride); + // Get a lease on the root PyBuffer (read-only) + this.root = root.getBuffer(FULL_RO); + } + + @Override + protected PyBuffer getRoot() { + return root; + } + } +} diff --git a/src/org/python/core/buffer/Strided1DWritableBuffer.java b/src/org/python/core/buffer/Strided1DWritableBuffer.java --- a/src/org/python/core/buffer/Strided1DWritableBuffer.java +++ b/src/org/python/core/buffer/Strided1DWritableBuffer.java @@ -1,5 +1,6 @@ package org.python.core.buffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.PyException; @@ -14,67 +15,54 @@ * Provide an instance of Strided1DWritableBuffer on a particular array of bytes * specifying a starting index, the number of items in the result, and a byte-indexing stride. * The result of byteAt(i) will be equal to storage[index0+stride*i] - * (whatever the sign of stride>0), valid for 0<=i<length. + * (whatever the sign of stride>0), valid for 0<=i<count. *

* The constructed PyBuffer meets the consumer's expectations as expressed in the * flags argument, or an exception will be thrown if these are incompatible with * the type (e.g. the consumer does not specify that it understands the strides array). Note * that the actual range in the storage array, the lowest and highest index, is not - * explicitly passed, but is implicit in index0, length and + * explicitly passed, but is implicit in index0, count and * stride. The caller is responsible for checking these fall within the array, or * the sub-range the caller is allowed to use. * * @param flags consumer requirements + * @param obj exporting object (or null) * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] - * @param length number of items in the slice - * @param stride in between successive elements of the new PyBuffer + * @param count number of items in the slice + * @param stride byte-index distance from one element to the next in the new PyBuffer * @throws NullPointerException if storage is null - * @throws ArrayIndexOutOfBoundsException if index0, length and + * @throws ArrayIndexOutOfBoundsException if index0, count and * stride are inconsistent with storage.length * @throws PyException (BufferError) when expectations do not correspond with the type */ - public Strided1DWritableBuffer(int flags, byte[] storage, int index0, int length, int stride) - throws ArrayIndexOutOfBoundsException, NullPointerException, PyException { - super(storage, index0, length, stride); + public Strided1DWritableBuffer(int flags, BufferProtocol obj, byte[] storage, int index0, + int count, int stride) throws ArrayIndexOutOfBoundsException, NullPointerException, + PyException { + super(obj, storage, index0, count, stride); addFeatureFlags(WRITABLE); checkRequestFlags(flags); // Check request is compatible with type } + /** + * {@inheritDoc} + *

+ * Declared final returning true in + * Strided1DWritableBuffer to make checks unnecessary. + */ @Override - public boolean isReadonly() { + public final boolean isReadonly() { return false; } + /** Do nothing: the buffer is writable. */ @Override - public void storeAt(byte value, int index) throws IndexOutOfBoundsException, PyException { - storage[index0 + index * stride] = value; - } + protected final void checkWritable() {} - /** - * {@inheritDoc} Strided1DWritableBuffer provides a version optimised for strided - * bytes in one dimension. - */ @Override - public void copyFrom(byte[] src, int srcPos, int destIndex, int length) - throws IndexOutOfBoundsException, PyException { - - // Data is here in the buffers - int s = srcPos; - int d = index0 + destIndex * stride; - - // Strategy depends on whether items are laid end-to-end or there are gaps - if (stride == 1) { - // Straight copy of contiguous bytes - System.arraycopy(src, srcPos, storage, d, length); - - } else { - // Non-contiguous copy: single byte items - int limit = d + length * stride; - for (; d != limit; d += stride) { - storage[d] = src[s++]; - } - } + protected void storeAtImpl(byte value, int byteIndex) { + // Implement directly and don't ask whether read-only + storage[byteIndex] = value; } /** @@ -84,17 +72,17 @@ * slice. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { - if (length > 0) { + if (count > 0) { // Translate start relative to underlying buffer int compStride = this.stride * stride; int compIndex0 = index0 + start * this.stride; // Construct a view, taking a lock on the root object (this or this.root) - return new SlicedView(getRoot(), flags, storage, compIndex0, length, compStride); + return new SlicedView(getRoot(), flags, storage, compIndex0, count, compStride); } else { - // Special case for length==0 where above logic would fail. Efficient too. + // Special case for count==0 where above logic would fail. Efficient too. return new ZeroByteBuffer.View(getRoot(), flags); } } @@ -115,14 +103,14 @@ * @param flags consumer requirements * @param storage raw byte array containing exported data * @param index0 index into storage of item[0] - * @param len number of items in the slice + * @param count number of items in the sliced view * @param stride in between successive elements of the new PyBuffer * @throws PyException (BufferError) when expectations do not correspond with the type */ - public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int len, int stride) - throws PyException { + public SlicedView(PyBuffer root, int flags, byte[] storage, int index0, int count, + int stride) throws PyException { // Create a new on the buffer passed in (part of the root) - super(flags, storage, index0, len, stride); + super(flags, root.getObj(), storage, index0, count, stride); // Get a lease on the root PyBuffer (writable) this.root = root.getBuffer(FULL); } @@ -131,13 +119,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } - } - } diff --git a/src/org/python/core/buffer/ZeroByteBuffer.java b/src/org/python/core/buffer/ZeroByteBuffer.java --- a/src/org/python/core/buffer/ZeroByteBuffer.java +++ b/src/org/python/core/buffer/ZeroByteBuffer.java @@ -1,5 +1,6 @@ package org.python.core.buffer; +import org.python.core.BufferProtocol; import org.python.core.PyBuffer; import org.python.core.PyException; @@ -12,29 +13,32 @@ * operations like {@link #copyTo(byte[], int)}) and {@link #toString()} efficiently do nothing, * instead of calling complicated logic that finally does nothing. */ -public class ZeroByteBuffer extends BaseBuffer { +public class ZeroByteBuffer extends BaseArrayBuffer { /** Shared instance of a zero-length storage. */ private static final byte[] EMPTY = new byte[0]; - /** Array containing a single zero for the length */ - protected static final int[] SHAPE = {0}; - /** * Construct an instance of a zero-length buffer, choosing whether it should report itself to be - * read-only through {@link #isReadonly()}. This is moot, as any attempt to write to it produces - * an {@link IndexOutOfBoundsException}, but it is less surprising for client code that may ask, - * if the readability follows that of the object from which the buffer is derived. + * read-only through {@link #isReadonly()} or as having a backing array through + * {@link #hasArray()}. These properties are moot, as any attempt to write to the pretended + * backing array produces an {@link IndexOutOfBoundsException}, but it is less surprising for + * client code that may ask, if the results are customary for the exporting object. * * @param flags consumer requirements - * @param readonly set true if readonly - * @throws PyException (BufferError) when expectations do not correspond with the type + * @param obj exporting object (or null) + * @param readonly set true if not to be considered writable + * @param hasArray set true if to be considered as backed by an array + * @throws PyException (BufferError) when client expectations do not correspond with the type */ - public ZeroByteBuffer(int flags, boolean readonly) throws PyException { - super(CONTIGUITY | SIMPLE | (readonly ? 0 : WRITABLE)); - this.storage = EMPTY; // Empty array - this.shape = SHAPE; // {0} - this.strides = SimpleBuffer.SIMPLE_STRIDES; // {1} + public ZeroByteBuffer(int flags, BufferProtocol obj, boolean readonly, boolean hasArray) + throws PyException { + super(EMPTY, CONTIGUITY | (readonly ? 0 : WRITABLE), 0, 0, 1); + this.obj = obj; + if (!hasArray) { + // super() knows we have an array, but this truth is inconvenient here. + removeFeatureFlags(AS_ARRAY); + } checkRequestFlags(flags); } @@ -47,7 +51,7 @@ * In a ZeroByteBuffer, the index is always out of bounds. */ @Override - protected int calcIndex(int index) throws IndexOutOfBoundsException { + public int byteIndex(int index) throws IndexOutOfBoundsException { // This causes all access to the bytes in to throw (since BaseBuffer calls it). throw new IndexOutOfBoundsException(); } @@ -56,7 +60,7 @@ * In a ZeroByteBuffer, if the dimensions are right, the index is out of bounds anyway. */ @Override - protected int calcIndex(int... indices) throws IndexOutOfBoundsException { + public int byteIndex(int... indices) throws IndexOutOfBoundsException { // Bootless dimension check takes precedence (for consistency with other buffers) checkDimension(indices); // This causes all access to the bytes to throw (since BaseBuffer calls it). @@ -79,30 +83,34 @@ * In a ZeroByteBuffer, there is simply nothing to copy. */ @Override - public void copyTo(int srcIndex, byte[] dest, int destPos, int length) + public void copyTo(int srcIndex, byte[] dest, int destPos, int count) throws IndexOutOfBoundsException, PyException { // Nothing to copy } /** - * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source length - * is zero. + * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source count is + * zero. */ @Override - public void copyFrom(byte[] src, int srcPos, int destIndex, int length) + public void copyFrom(byte[] src, int srcPos, int destIndex, int count) throws IndexOutOfBoundsException, PyException { - if (length > 0) { + if (this.isReadonly()) { + throw notWritable(); + } else if (count > 0) { throw new IndexOutOfBoundsException(); } } /** - * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source length - * is zero. + * In a ZeroByteBuffer, there is no room for anything, so this throws unless the source count is + * zero. */ @Override public void copyFrom(PyBuffer src) throws IndexOutOfBoundsException, PyException { - if (src.getLen() > 0) { + if (this.isReadonly()) { + throw notWritable(); + } else if (src.getLen() > 0) { throw new IndexOutOfBoundsException(); } } @@ -112,8 +120,8 @@ * as a result, with the export count incremented. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length) { - if (start == 0 && length <= 0) { + public PyBuffer getBufferSlice(int flags, int start, int count) { + if (start == 0 && count <= 0) { return this.getBuffer(flags); } else { throw new IndexOutOfBoundsException(); @@ -125,9 +133,9 @@ * as a result, with the export count incremented. */ @Override - public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { - // It can't matter what the stride is since length is zero, or there's an error. - return getBufferSlice(flags, start, length); + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { + // It can't matter what the stride is since count is zero, or there's an error. + return getBufferSlice(flags, start, count); } /** @@ -135,6 +143,7 @@ *

* The implementation in ZeroByteBuffer efficiently returns an empty buffer. */ + @SuppressWarnings("deprecation") @Override public Pointer getBuf() { // Has to be new because the client is allowed to manipulate the contents. @@ -170,7 +179,7 @@ */ public View(PyBuffer root, int flags) { // Create a new ZeroByteBuffer on who-cares-what byte array - super(flags, root.isReadonly()); + super(flags, root.getObj(), root.isReadonly(), root.hasArray()); // But we still have to get a lease on the root PyBuffer this.root = root.getBuffer(FULL_RO); } @@ -179,11 +188,5 @@ protected PyBuffer getRoot() { return root; } - - @Override - public void releaseAction() { - // We have to release the root too if ours was final. - root.release(); - } } } diff --git a/src/org/python/modules/_io/PyIOBase.java b/src/org/python/modules/_io/PyIOBase.java --- a/src/org/python/modules/_io/PyIOBase.java +++ b/src/org/python/modules/_io/PyIOBase.java @@ -778,7 +778,7 @@ // None of the above: complain throw tailoredTypeError("read-write buffer", obj); } - return new SimpleStringBuffer(PyBUF.SIMPLE, s); + return new SimpleStringBuffer(PyBUF.SIMPLE, null, s); } } @@ -910,18 +910,17 @@ + "fp is closed after the suite of the with statement is complete:\n" + "\n" + "with open('spam.txt', 'r') as fp:\n" + " fp.write('Spam and eggs!')\n"; - /* Traverseproc implementation */ @Override public int traverse(Visitproc visit, Object arg) { - //closer cannot be null + // closer cannot be null if (closer.sys != null) { - int retVal = visit.visit(closer.sys, arg); + int retVal = visit.visit(closer.sys, arg); if (retVal != 0) { return retVal; } } - //__dict__ cannot be null + // __dict__ cannot be null return visit.visit(__dict__, arg); } diff --git a/tests/java/org/python/core/BaseBytesTest.java b/tests/java/org/python/core/BaseBytesTest.java --- a/tests/java/org/python/core/BaseBytesTest.java +++ b/tests/java/org/python/core/BaseBytesTest.java @@ -203,6 +203,7 @@ * * @see junit.framework.TestCase#setUp() */ + @Override protected void setUp() throws Exception { super.setUp(); random = new Random(20120310L); @@ -595,7 +596,7 @@ */ public MyBytes(BufferProtocol value) { super(TYPE); - init((BufferProtocol)value.getBuffer(PyBUF.SIMPLE)); + init(value.getBuffer(PyBUF.SIMPLE)); } /** @@ -787,7 +788,7 @@ @Override public PyBuffer getBuffer(int flags) { - return new SimpleBuffer(flags, store); + return new SimpleBuffer(flags, this, store); } } @@ -824,7 +825,7 @@ return; } for (int i = 0; i < n; i++) { - int c = 0xff & ((int)s[pos + i]); + int c = 0xff & (s[pos + i]); if (c == 0) { c = '.'; } else if (Character.isISOControl(c)) { diff --git a/tests/java/org/python/core/ByteBufferTestSupport.java b/tests/java/org/python/core/ByteBufferTestSupport.java new file mode 100644 --- /dev/null +++ b/tests/java/org/python/core/ByteBufferTestSupport.java @@ -0,0 +1,585 @@ +package org.python.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * Common apparatus for tests involving byte[] and java.nio.ByteBuffer + * material, in particular the tests of {@link PyBuffer} implementations. A test case that extends + * this class will be equipped with additional assertion methods and a class to represent + * byte[] test material in several forms. + */ +public class ByteBufferTestSupport { + + /** + * Class to hold test material representing the same sequence of values 0..255 in several + * different ways. + */ + protected static class ByteMaterial { + + /** Length in bytes (length of every array in this material). */ + final int length; + /** The byte values. */ + byte[] bytes; + /** The byte values individually as ints. */ + int[] ints; + /** The byte values treated as characters (unicode < 256). */ + String string; + + /** Construct from int array. */ + public ByteMaterial(int[] a) { + ints = a.clone(); + length = replicate(); + } + + /** Construct from String. */ + public ByteMaterial(String s) { + ints = new int[s.length()]; + for (int i = 0; i < ints.length; i++) { + ints[i] = 0xff & s.charAt(i); + } + length = replicate(); + } + + /** Construct from byte array. */ + public ByteMaterial(byte[] b) { + ints = new int[b.length]; + for (int i = 0; i < ints.length; i++) { + ints[i] = 0xff & b[i]; + } + length = replicate(); + } + + /** Construct from pattern on values (used modulo 256). */ + public ByteMaterial(int start, int count, int inc) { + ints = new int[count]; + int x = start; + for (int i = 0; i < count; i++) { + ints[i] = x; + x = (x + inc) & 0xff; + } + length = replicate(); + } + + /** + * Once the integer value array {@link #ints} has been initialised, fill the others from it. + * + * @return length of (all) arrays in units + */ + private int replicate() { + int n = ints.length; + bytes = new byte[n]; + StringBuilder sb = new StringBuilder(n); + + for (int i = 0; i < n; i++) { + int x = ints[i]; + bytes[i] = (byte)x; + sb.appendCodePoint(x); + } + string = sb.toString(); + return n; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(100); + sb.append("byte[").append(length).append("]={ "); + for (int i = 0; i < length; i++) { + if (i > 0) { + sb.append(", "); + } + if (i >= 5) { + sb.append(" ..."); + break; + } else { + sb.append(ints[i]); + } + } + sb.append(" }"); + return sb.toString(); + } + + /** + * @return a copy of the bytes array (that the client is allowed to modify) + */ + byte[] getBytes() { + return bytes.clone(); + } + + /** + * @return a buffer on a copy of the bytes array (that the client is allowed to modify) + */ + ByteBuffer getBuffer() { + return ByteBuffer.wrap(getBytes()); + } + + /** + * Create material equivalent to a slice of this material. This may act as a reference + * result for testing slice operations. + * + * @param start first byte-index to include + * @param length number of items + * @param stride between byte-indices + * @return ByteMaterial in which the arrays are a slice of this one + */ + ByteMaterial slice(int start, int length, int stride) { + return new ByteMaterial(sliceBytes(bytes, 1, start, length, stride)); + } + + /** + * Create material equivalent to a slice of this material. This may act as a reference + * result for testing slice operations. + * + * @param start first byte-index to include + * @param itemsize number of consecutive bytes treated as one item + * @param length number of items + * @param stride between byte-indices + * @return ByteMaterial in which the arrays are a slice of this one + */ + ByteMaterial slice(int itemsize, int start, int length, int stride) { + return new ByteMaterial(sliceBytes(bytes, itemsize, start, length, stride)); + } + } + + /** + * Create a byte array that is a strided copy of the one passed in. The specifications are + * assumed correct for the size of that array. + * + * @param b source array + * @param start first index to include + * @param length number of indices + * @param stride between indices + * @return slice of b + */ + protected static byte[] sliceBytes(byte[] b, int start, int length, int stride) { + return sliceBytes(b, 1, start, length, stride); + } + + /** + * Create a multi-byte item array that is a strided copy of the one passed in. The + * specifications are assumed correct for the size of that array. + * + * @param b source array + * @param itemsize number of consecutive bytes treated as one item + * @param start first byte-index to include + * @param length number of indices to visit (items to copy) + * @param stride between byte-indices + * @return slice of b + */ + protected static byte[] sliceBytes(byte[] b, int itemsize, int start, int length, int stride) { + byte[] a = new byte[length]; + for (int i = 0, j = start; i < length; i++, j += stride) { + for (int k = 0; k < itemsize; k++) { + a[i + k] = b[j + k]; + } + } + return a; + } + + /** + * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte + * array, when that ByteBuffer is obtained from a contiguous PyBuffer. + * Let bb[i] denote bb.get(bb.position()+i), by analogy with a C + * pointer. It is required that bb[k] == expected[k], for every index + * k in expected. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param bb result to test + */ + static void assertBytesEqual(String message, byte[] expected, ByteBuffer bb) { + // Use the position-advancing buffer get() + byte[] actual = new byte[expected.length]; + bb.get(actual); + assertBytesEqual(message, expected, actual); + } + + /** + * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte + * array, when that ByteBuffer is obtained from a striding PyBuffer. + * Let bb[i] denote bb.get(bb.position()+i), by analogy with a C + * pointer. It is required that bb[k*stride] == expected[k], for every index + * k in expected. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param bb result to test + * @param stride in the buffer bb + */ + static void assertBytesEqual(String message, byte[] expected, ByteBuffer bb, int stride) { + assertBytesEqual(message, expected, 0, expected.length, bb, stride); + } + + /** + * Custom assert method comparing the bytes in an NIO {@link ByteBuffer} to those in a byte + * array, when that ByteBuffer is obtained from a striding PyBuffer. + * Let bb[i] denote bb.get(bb.position()+i), by analogy with a C + * pointer. It is required that bb[k*stride] == expected[expectedStart+k], for + * k=0 to n-1. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param expectedStart where to start the comparison in expected + * @param n number of bytes to test + * @param bb result to test + * @param stride in the buffer bb + */ + static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, + ByteBuffer bb, int stride) { + // Note that this approach leaves the buffer position unmodified + int p = bb.position(); + byte[] actual = new byte[n]; + for (int k = 0; k < n; k++, p += stride) { + actual[k] = bb.get(p); + } + assertBytesEqual(message, expected, expectedStart, n, actual, 0); + } + + /** + * Custom assert method comparing byte arrays: values in actual[] must match all + * those in expected[], and they must be the same length. + * + * @param message to issue on failure + * @param expected expected byte array + * @param actual result to test + */ + static void assertBytesEqual(String message, byte[] expected, byte[] actual) { + assertEquals(message + " (array size)", expected.length, actual.length); + assertBytesEqual(message, expected, 0, expected.length, actual, 0, 1); + } + + /** + * Custom assert method comparing byte arrays. It is required that + * actual[k] == expected[k], for k=0 to expected.length-1 + * . If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param actual result to test + * @param actualStart where to start the comparison in actual + */ + static void assertBytesEqual(String message, byte[] expected, byte[] actual, int actualStart) { + assertBytesEqual(message, expected, 0, expected.length, actual, actualStart, 1); + } + + /** + * Custom assert method comparing byte arrays. It is required that + * actual[actualStart+k] == expected[expectedStart+k], for k=0 to + * n-1. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param expectedStart where to start the comparison in expected + * @param n number of bytes to test + * @param actual result to test + * @param actualStart where to start the comparison in actual + */ + protected static void assertBytesEqual(String message, byte[] expected, int expectedStart, + int n, byte[] actual, int actualStart) { + assertBytesEqual(message, expected, expectedStart, n, actual, actualStart, 1); + } + + /** + * Custom assert method comparing byte arrays. It is required that + * actual[actualStart+k*stride] == expected[expectedStart+k], for k=0 + * to n-1. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param expectedStart where to start the comparison in expected + * @param n number of bytes to test + * @param actual result to test + * @param actualStart where to start the comparison in actual + * @param stride spacing of bytes in actual array + */ + static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, + byte[] actual, int actualStart, int stride) { + + if (actualStart < 0) { + fail(message + " (start<0 in result)"); + + } else if (expectedStart < 0) { + fail(message + " (start<0 in expected result): bug in test?"); + + } else if (actualStart + (n - 1) * stride + 1 > actual.length) { + fail(message + " (result too short)"); + + } else if (expectedStart + n > expected.length) { + fail(message + " (expected result too short): bug in test?"); + + } else { + // Should be safe to compare the values + int i = actualStart, j, jLimit = expectedStart + n; + for (j = expectedStart; j < jLimit; j++) { + if (actual[i] != expected[j]) { + break; + } + i += stride; + } + + // If we stopped early, diagnose the problem + if (j < jLimit) { + byte[] a = Arrays.copyOfRange(actual, actualStart, actualStart + n); + byte[] e = Arrays.copyOfRange(expected, expectedStart, expectedStart + n); + System.out.println(" expected:" + Arrays.toString(e)); + System.out.println(" actual:" + Arrays.toString(a)); + System.out.println(" _actual_:" + Arrays.toString(actual)); + fail(message + " (byte at " + j + ")"); + } + } + } + + /** + * Customised assert method comparing a int arrays: values in the actual value starting at + * actual[offset] must match all those in expected[], and there must be enough of them. + * + * @param message to issue on failure + * @param expected expected array + * @param actual result to test + * @param offset where to start the comparison in actual + */ + static void assertIntsEqual(String message, int[] expected, int[] actual, int offset) { + int n = expected.length; + if (offset < 0) { + fail(message + " (offset<0)"); + } else if (offset + n > actual.length) { + fail(message + " (too short)"); + } else { + // Should be safe to compare the values + int i = offset, j; + for (j = 0; j < n; j++) { + if (actual[i++] != expected[j]) { + break; + } + } + if (j < n) { + System.out.println(" expected:" + Arrays.toString(expected)); + System.out.println(" actual:" + Arrays.toString(actual)); + fail(message + " (int at " + j + ")"); + } + } + } + + /** + * Customised assert method comparing a int arrays: int in the actual value must match all those + * in expected[], and there must be the same number of them. + * + * @param message to issue on failure + * @param expected expected array + * @param actual result to test + */ + protected static void assertIntsEqual(String message, int[] expected, int[] actual) { + int n = expected.length; + assertEquals(message, n, actual.length); + // Should be safe to compare the values + int j; + for (j = 0; j < n; j++) { + if (actual[j] != expected[j]) { + break; + } + } + if (j < n) { + System.out.println(" expected:" + Arrays.toString(expected)); + System.out.println(" actual:" + Arrays.toString(actual)); + fail(message + " (int at " + j + ")"); + } + } + + /** + * Method comparing byte arrays after a read (or view creation) operation involving a slice. + *

+ * The invariant asserted must be explained carefully because of its generality. Suppose there + * to be three arrays of bytes a, b and c. Let a represent the state + * of some byte storage of length L before an operation. Let b represent the state + * of the same storage after an operation. Let c be related as follows. + *

+ * c is the result, representing n blocks of u bytes copied from the + * storage, the kth block starting at position s+kp in the storage and at + * t+ku in c. c is of length M≥nu, and we assume + * 0≤s+kp<L. After a read operation, it is required that: + *

    + *
  1. c[t+iu+j] = b[s+ip+j] for 0≤i<n and 0≤j<u, and
  2. + *
  3. a[k] = b[k] for 0≤k<L. + *
+ *

+ * + * @param a storage state before the operation (typically reference data) + * @param b storage state after the operation (typically from object under test) + * @param c bytes read + * @param t index in c of the start byte of item 0 + * @param n number of items + * @param u number of consecutive bytes per item + * @param s index in b of the start byte of item 0 + * @param p the distance in b between the start bytes of successive items + */ + static void checkReadCorrect(byte[] a, byte[] b, byte[] c, int t, int n, int u, int s, int p) { + // Check the b is the same as a + assertEquals("Storage size differs from reference", a.length, b.length); + for (int k = 0; k < b.length; k++) { + if (a[k] != b[k]) { + fail("Stored data changed during read"); + } + } + // Check slice read out + checkEqualInSlice(b, c, t, n, u, s, p); + } + + /** + * Method comparing byte arrays where a change operation has taken place involving a slice. + *

+ * The invariant asserted must be explained carefully because of its generality. Suppose there + * to be three arrays of bytes a, b and c. Let a represent the state + * of some byte storage of length L before an operation. Let b represent the state + * of the same storage after an operation. Let c be related as follows. + *

+ * c is the source, contaning at index t, n blocks of u bytes copied + * to the storage. As before, the kth block starts at position s+kp in the storage + * and at t+ku in c. c is of length M≥t+nu, and we assume + * 0≤s+kp<L. After a write operation, it is required that: + *

    + *
  1. c[t+iu+j] = b[s+ip+j] for 0≤i<n and 0≤j<u, and
  2. + *
  3. a[k] = b[k] for 0≤k<L and k≠s+ip+j for any choice of + * and j where 0≤i<n and 0≤j<u. + *
+ * Note that the first of these is the same as for a read and the second requires equality + * "everywhere else". + * + * @param a storage state before the operation (typically reference data) + * @param b storage state after the operation (typically from object under test) + * @param c bytes written + * @param t index in c of the start byte of item 0 + * @param n number of items + * @param u number of consecutive bytes per item + * @param s index in b of the start byte of item 0 + * @param p the distance in b between the start bytes of successive items + */ + static void checkWriteCorrect(byte[] a, byte[] b, byte[] c, int t, int n, int u, int s, int p) { + assertEquals("Stored size has changed", a.length, b.length); + checkEqualInSlice(b, c, t, n, u, s, p); + checkUnchangedElsewhere(a, b, n, u, s, p); + } + + /** + * Method comparing bytes in a slice pattern of one byte array to bytes taken consecutively in + * another array. This is needed in testing when bytes have been copied into or out of an array + * slice. + *

+ * Let b represent the state of the byte storage of length L after the copy + * operation (the sliced array). Let c be a source or destination array, a section of + * which at index t represents n blocks of u bytes copied to or from the + * storage. c is of length at least t+nu. The kth block starts at position + * s+kp in the storage b and at t+ku in c, and we assume + * 0≤s+kp<L. After a write operation, it is required that: c[t+iu+j] = + * b[s+ip+j] for 0≤i<n and 0≤j<u. + * + * + * @param b storage state after the operation (typically from object under test) + * @param c bytes written + * @param t index in c of the start byte of item 0 + * @param n number of items + * @param u number of consecutive bytes per item + * @param s index in b of the start byte of item 0 + * @param p the distance in b between the start bytes of successive items + */ + static void checkEqualInSlice(byte[] b, byte[] c, int t, int n, int u, int s, int p) { + // Check correct use of the test + checkSliceArgs(b, c, t, n, u, s, p); + + // Check the data in copied units (and p-u bytes following) + for (int i = 0; i < n; i++) { + int ci = t + i * u, bi = s + i * p; + for (int j = 0; j < u; j++, bi++, ci++) { + // Compare corresponding bytes of this unit in c and b + if (c[ci] != b[bi]) { + fail(String.format("contiguous data at %d not equal to buffer at %d", ci, bi)); + } + } + } + } + + /** + * Method comparing the before and after state of the parts of a byte array that should be + * untouched where a change operation has taken place involving a slice. + *

+ * Let a represent the state of some byte storage of length L before an operation. + * Let b represent the state of the same storage after an operation. After a write + * operation, it is required that: a[k] = b[k] for 0≤k<L and + * k≠s+ip+j for any choice of and j where 0≤i<n and + * 0≤j<u. + *

+ * Note that requires equality "everywhere else" than in the slice defined by n units of + * size u starting at s. + * + * @param a storage state before the operation (typically reference data) + * @param b storage state after the operation (typically from object under test) + * @param n number of items + * @param u number of consecutive bytes per item + * @param s index in b of the start byte of item 0 + * @param p the distance in b between the start bytes of successive items + */ + static void checkUnchangedElsewhere(byte[] a, byte[] b, int n, int u, int s, int p) { + // Check correct use of the test + assertEquals("Stored size has changed", a.length, b.length); + assertFalse("Unit size exceeds spacing", u > p && u + p > 0); + String bufferChangedAt = "buffer changed at %d (outside slice)"; + + int absp, low, high; + + if (n < 1) { + // No elements: check whole array. + absp = low = high = 0; + } else if (p >= 0) { + // Stride is forwards in the range (easy case) + absp = p; + // Lowest byte index in the data is byte 0 of first unit in slice + low = s; + // One after highest byte index is just beyond last byte of last unit in slice + high = s + (n - 1) * p + u; + } else { + // p<0: stride is backwards in the range (delicate case) + absp = -p; + // Lowest byte index in the data is byte 0 of last unit in slice + low = s + (n - 1) * p; + // One after highest byte index is just beyond last byte of first unit in slice + high = s + u; + } + + // Visit each copied unit (from low to high byte index) except the highest. + for (int i = 0; i < n - 1; i++) { + int bi = low + i * absp + u; + // Check there was no change to the absp-u bytes following unit in b + for (int j = u; j < absp; j++, bi++) { + if (b[bi] != a[bi]) { + fail(String.format(bufferChangedAt, bi)); + } + } + } + + // Check that b[0:low] is unchanged + for (int k = 0; k < low; k++) { + if (b[k] != a[k]) { + fail(String.format(bufferChangedAt, k)); + } + } + + // Check that [high:] is unchanged + for (int k = Math.max(high, 0); k < b.length; k++) { + if (b[k] != a[k]) { + fail(String.format(bufferChangedAt, k)); + } + } + } + + /** Common code for checkReadCorrect and checkWriteCorrect. */ + private static void checkSliceArgs(byte[] b, byte[] c, int t, int n, int u, int s, int p) { + // Check correct use of the test + assertFalse("Slice data less than n units", c.length < t + n * u); + assertFalse("Slice data exceeds destination", n * u > b.length); + assertFalse("Unit size exceeds spacing", u > p && u + p > 0); + } + +} \ No newline at end of file diff --git a/tests/java/org/python/core/PyBufferNIOTest.java b/tests/java/org/python/core/PyBufferNIOTest.java new file mode 100644 --- /dev/null +++ b/tests/java/org/python/core/PyBufferNIOTest.java @@ -0,0 +1,305 @@ +package org.python.core; + +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.List; + +import org.junit.runners.Parameterized.Parameters; +import org.python.core.ByteBufferTestSupport.ByteMaterial; +import org.python.core.PyBufferTestSupport.ExporterFactory; +import org.python.core.PyBufferTestSupport.TestSpec; +import org.python.core.PyBufferTestSupport.WritableExporterFactory; +import org.python.core.buffer.BaseBuffer; +import org.python.core.buffer.SimpleNIOBuffer; + +public class PyBufferNIOTest extends PyBufferTest { + + public PyBufferNIOTest(TestSpec spec) { + super(spec); + } + + /** + * Generate test data to be held in the testing framework and used to construct tests. This + * method is called once by the test framework. Each element of the returned collection is a + * specification that becomes the arguments to the constructor when JUnit prepares to invoke a + * test. + *

+ * Internally, this method creates a small number of instances of the object types whose + * PyBuffer export mechanism is to be tested. Each is paired with a reference value + * represented in several forms. The PyBufferTestSupport class then multiplies + * these by creating a selection of feasible sliced views, the whole collection of root and + * slice objects being returned. + * + * @return generated list of test data + */ + @Parameters + public static Collection genTestSpecs() { + + PyBufferTestSupport s = new PyBufferTestSupport(sliceLengths, sliceSteps); + + // Tests using local types of exporter + + ExporterFactory rollYourOwnExporter = new WritableExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new RollYourOwnExporter(m.getBuffer()); + } + + }; + s.add(rollYourOwnExporter, byteMaterial); + s.add(rollYourOwnExporter, emptyMaterial); + + // All combinations of heap/direct, writable and empty/small/large (I'm so thorough!) + + ExporterFactory readonlyHeapNIOExporter = new TestNIOExporterFactory(false, false); + s.add(readonlyHeapNIOExporter, emptyMaterial); + s.add(readonlyHeapNIOExporter, byteMaterial); + s.add(readonlyHeapNIOExporter, longMaterial); + + ExporterFactory writableHeapNIOExporter = new TestNIOExporterFactory(true, false); + s.add(writableHeapNIOExporter, emptyMaterial); + s.add(writableHeapNIOExporter, byteMaterial); + s.add(writableHeapNIOExporter, longMaterial); + + ExporterFactory readonlyDirectNIOExporter = new TestNIOExporterFactory(false, true); + s.add(readonlyDirectNIOExporter, emptyMaterial); + s.add(readonlyDirectNIOExporter, byteMaterial); + s.add(readonlyDirectNIOExporter, longMaterial); + + ExporterFactory writableDirectNIOExporter = new TestNIOExporterFactory(true, true); + s.add(writableDirectNIOExporter, emptyMaterial); + s.add(writableDirectNIOExporter, byteMaterial); + s.add(writableDirectNIOExporter, longMaterial); + + // Return the generated test data + + List ret = s.getTestData(); + if (PRINT_KEY) { + int key = 0; + for (TestSpec[] r : ret) { + TestSpec spec = r[0]; + System.out.printf("%6d : %s\n", key++, spec.toString()); + } + } + return ret; + } + + /* + * -------------------------------------------------------------------------------------------- + * A series of custom exporters that use a java.nio.ByteBuffer to store and export their + * implementation data. + * -------------------------------------------------------------------------------------------- + */ + /** + * A class to act as an exporter that uses the SimpleBuffer. The exporter shares a single + * exported buffer between all consumers and needs to take any action immediately when that + * buffer is finally released. You are most likely to use this approach with an exporting object + * type that modifies its behaviour while there are active exports, but where it is worth + * avoiding the cost of duplicate buffers. This is the case with PyByteArray, which prohibits + * operations that would resize it, while there are outstanding exports. + */ + private static class TestNIOExporter extends TestableExporter { + + protected ByteBuffer storage; + + /** + * Construct a simple exporter from the bytes supplied. + * + * @param storage + */ + public TestNIOExporter(ByteBuffer storage) { + this.storage = storage; + } + + @Override + public PyBuffer getBuffer(int flags) { + // If we have already exported a buffer it may still be available for re-use + BaseBuffer pybuf = getExistingBuffer(flags); + if (pybuf == null) { + // No existing export we can re-use + pybuf = new SimpleNIOBuffer(flags, this, storage) { + + @Override + protected void releaseAction() { + export = null; // Final release really is final (not reusable) + } + }; + + // Hold a reference for possible re-use + export = new WeakReference(pybuf); + } + return pybuf; + } + + } + + /** + * A factory for exporting objects to be used in the tests. These objects use a + * ByteBuffer for their exported representation, and the factory is programmed on + * creation to whether these buffers should be writable or direct. + */ + static class TestNIOExporterFactory implements ExporterFactory { + + final boolean writable; + final boolean isDirect; + + TestNIOExporterFactory(boolean writable, boolean isDirect) { + this.writable = writable; + this.isDirect = isDirect; + } + + public boolean isWritable() { + return writable; + } + + public boolean isDirect() { + return isDirect; + } + + @Override + public BufferProtocol make(ByteMaterial m) { + ByteBuffer bb = m.getBuffer(); + if (isDirect) { + // Replace bb with a direct buffer containing the same bytes + ByteBuffer direct = ByteBuffer.allocateDirect(bb.capacity()); + direct.put(bb).flip(); + bb = direct; + } + if (!writable) { + bb = bb.asReadOnlyBuffer(); + } + return new TestNIOExporter(bb); + } + + @Override + public boolean isReadonly() { + return !writable; + } + + @Override + public boolean hasArray() { + return !isDirect && writable; + } + + } + + /** A class to act as an exporter that uses the RollYourOwnNIOBuffer class. */ + private static class RollYourOwnExporter extends TestableExporter { + + protected ByteBuffer storage; + + public RollYourOwnExporter(ByteBuffer storage) { + this.storage = storage; + } + + @Override + public PyBuffer getBuffer(int flags) { + // If we have already exported a buffer it may still be available for re-use + BaseBuffer pybuf = getExistingBuffer(flags); + if (pybuf == null) { + // No existing export we can re-use + pybuf = new RollYourOwnNIOBuffer(flags, this, storage); + // Hold a reference for possible re-use + export = new WeakReference(pybuf); + } + return pybuf; + } + + } + + /** + * Minimal extension of BaseBuffer in order to test the default implementations there. They're + * slow, so mostly we override them in the implementations BaseArrayBuffer and BaseNIOBuffer, + * but they still have to be correct. The class represents a one-dimensional, strided array of + * bytes, so it can represent a slice of itself. + */ + private static class RollYourOwnNIOBuffer extends BaseBuffer { + + final static int FEATURES = PyBUF.WRITABLE | PyBUF.AS_ARRAY; + + final ByteBuffer storage; + final PyBuffer root; + + /** + * Create a buffer view of a given ByteBuffer in which the data is the + * contiguous sequence of bytes from the position to the limit. + * + * @param flags consumer requirements + * @param obj exporting object (or null) + * @param storage buffer exported (from the position to the limit) + */ + public RollYourOwnNIOBuffer(int flags, BufferProtocol obj, ByteBuffer storage) { + this(flags, null /* =this */, obj, storage, storage.position(), storage.remaining(), 1); + } + + /** + * Construct a slice of a one-dimensional byte buffer. + * + * @param flags consumer requirements + * @param obj exporting object (or null) + * @param root on which release must be called when this is released + * @param storage buffer containing exported data + * @param index0 index into storage of item[0] + * @param count number of items in the slice + * @param stride in between successive elements of the new PyBuffer + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public RollYourOwnNIOBuffer(int flags, PyBuffer root, BufferProtocol obj, + ByteBuffer storage, int index0, int count, int stride) + throws IndexOutOfBoundsException, NullPointerException, PyException { + // Client will need to navigate using shape and strides if this is a slice + super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : STRIDES), // + index0, new int[] {count}, new int[] {stride}); + this.storage = storage.duplicate(); + + // Check the potential index range + if (count > 0) { + int end = index0 + (count - 1) * stride; + final int END = storage.capacity() - 1; + if (index0 < 0 || index0 > END || end < 0 || end > END) { + throw new IndexOutOfBoundsException(); + } + } + // Check client is compatible + checkRequestFlags(flags); + // Get a lease on the root PyBuffer (read-only). Last in case a check above fails. + if (root == null) { + this.root = this; + this.obj = obj; + } else { + this.root = root.getBuffer(FULL_RO); + this.obj = root.getObj(); + } + } + + @Override + protected PyBuffer getRoot() { + return root; + } + + @Override + public PyBuffer getBufferSlice(int flags, int start, int count, int stride) { + int newStart = index0 + start * strides[0]; + int newStride = strides[0] * stride; + return new RollYourOwnNIOBuffer(flags, root, null, storage, newStart, count, newStride); + } + + @Override + public ByteBuffer getNIOByteBufferImpl() { + return storage.duplicate(); + } + + @Override + protected byte byteAtImpl(int byteIndex) { + return storage.get(byteIndex); + } + + @Override + protected void storeAtImpl(byte value, int byteIndex) throws IndexOutOfBoundsException, + PyException { + storage.put(byteIndex, value); + } + } +} diff --git a/tests/java/org/python/core/PyBufferTest.java b/tests/java/org/python/core/PyBufferTest.java --- a/tests/java/org/python/core/PyBufferTest.java +++ b/tests/java/org/python/core/PyBufferTest.java @@ -1,16 +1,28 @@ package org.python.core; +import static org.junit.Assert.*; +import static org.python.core.ByteBufferTestSupport.assertIntsEqual; +import static org.python.core.PyBufferTestSupport.bytesFromByteAt; + import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.HashSet; -import java.util.LinkedList; +import java.util.Collection; import java.util.List; -import java.util.Set; -import junit.framework.TestCase; - +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.python.core.ByteBufferTestSupport.ByteMaterial; +import org.python.core.PyBufferTestSupport.ExporterFactory; +import org.python.core.PyBufferTestSupport.ReadonlyExporterFactory; +import org.python.core.PyBufferTestSupport.SlicedTestSpec; +import org.python.core.PyBufferTestSupport.TestSpec; +import org.python.core.PyBufferTestSupport.TestSpec.ObjectAndView; +import org.python.core.PyBufferTestSupport.WritableExporterFactory; import org.python.core.buffer.BaseBuffer; import org.python.core.buffer.SimpleBuffer; import org.python.core.buffer.SimpleStringBuffer; @@ -21,16 +33,18 @@ * Test the several implementations (and exporters) of the PyBuffer interface provided in the Jython * core. *

- * The approach is to create test material once that has the necessary variety in byte array values, - * then for each test, when the JUnit framework creates an instance of the function-specific test, - * to use this material to create instances of each read-only type and each writable type. Writable - * instance types go onto the lists buffersToRead and buffersToWrite, while read-only instances go - * onto the lists buffersToRead and buffersToFailToWrite. + * The approach is to create test material once (static initialisation) that has the necessary + * variety in byte array values. From these raw values, during a phase of (static) initialisation + * invoked by the JUnit framework, we create a rich set of root objects, and slices made from them, + * paired with the value those buffer views should have, represented as byte[] (and a few other + * types). These are BufferTestPair objects. The collection is the test data. *

- * In general, tests of methods that read data apply themselves to all the elements of the - * buffersToRead list, while tests of methods that write data apply themselves to all the elements - * of the buffersToWrite list and check that members of the buffersToFailToWrite list raise an - * exception. + * The JUnit framework will then construct an instance of this test using one + * BufferTestPair object from the test data, and call one test method. The + * initialisation of the test fixture with a BufferTestPair object provides the test + * method with a PyBuffer object on which to operate, and enough ancilliary information + * to deduce the expected outcome. In particular, it will be apparent whether write actions should + * succeed or raise an exception. *

* The Jython buffer API follows the structures of the CPython buffer API so that it supports in * principle the use of multi-dimensional, strided and indirect array structures as buffers. @@ -39,598 +53,754 @@ * N-dimensional cases, and some need a complete re-think. Sub-classing this test would probably be * a good way to extend it to a wider range. */ -public class PyBufferTest extends TestCase { + at RunWith(Parameterized.class) +public class PyBufferTest { /** Control amount of output. Instance variable so can be adjusted temporarily in test. */ protected int verbosity = 0; + /** Print a list of the test material. (From JUnit 4.12 use Parameters(name)). */ + protected static final boolean PRINT_KEY = true; + + /** Size of some large arrays. */ + static final int LONG = 1000; + + /** Lengths we will use if we can when slicing view */ + protected static final int[] sliceLengths = {1, 2, 5, 0, LONG / 4}; + + /** Step sizes we will use if we can when slicing view */ + protected static final int[] sliceSteps = {1, 2, 3, 7}; + + /** Exception raising requires the Jython interpreter to be initialised **/ + protected static PythonInterpreter interp = new PythonInterpreter(); + + /** The test material and a buffer created by the test-runner. */ + protected TestSpec spec; + protected ByteMaterial ref; + protected BufferProtocol obj; + protected PyBuffer view; + /** - * Generated constructor + * Construct an instance to run one test, using one set of test data. * - * @param name + * @param pair The test material and a buffer created by the test-runner. */ - public PyBufferTest(String name) { - super(name); + public PyBufferTest(TestSpec spec) { + this.spec = spec; + ref = spec.ref; + createObjAndView(); } - /** Sometimes we need the interpreter to be initialised **/ - PythonInterpreter interp; + /** + * Create (or re-create) the test object and view from the specification. Test cases that call a + * mutator repeatedly must call this each time in order to work with clean objects. + */ + protected void createObjAndView() { + TestSpec.ObjectAndView pair = spec.makePair(); + obj = pair.obj; + view = pair.view; + } /* * Values for initialising the exporters. */ - private static final ByteMaterial byteMaterial = new ByteMaterial(0, 16, 17); - private static final ByteMaterial abcMaterial = new ByteMaterial("abcdefgh"); - private static final ByteMaterial stringMaterial = new ByteMaterial("Mon c?t? f?cheux"); - private static final ByteMaterial emptyMaterial = new ByteMaterial(new byte[0]); - public static final int LONG = 1000; - private static final ByteMaterial longMaterial = new ByteMaterial(0, LONG, 5); + protected static final ByteMaterial byteMaterial = new ByteMaterial(10, 16, 3); + protected static final ByteMaterial abcMaterial = new ByteMaterial("abcdefgh"); + protected static final ByteMaterial stringMaterial = new ByteMaterial("Mon c?t? f?cheux"); + protected static final ByteMaterial emptyMaterial = new ByteMaterial(new byte[0]); + protected static final ByteMaterial longMaterial = new ByteMaterial(0, LONG, 5); - @Override - protected void setUp() throws Exception { - super.setUp(); + /** + * Generate test data to be held in the testing framework and used to construct tests. This + * method is called once by the test framework. Each element of the returned collection is a + * specification that becomes the arguments to the constructor when JUnit prepares to invoke a + * test. + *

+ * Internally, this method creates a small number of instances of the object types whose + * PyBuffer export mechanism is to be tested. Each is paired with a reference value + * represented in several forms. The PyBufferTestSupport class then multiplies + * these by creating a selection of feasible sliced views, the whole collection of root and + * slice objects being returned. + * + * @return generated list of test data + */ + @Parameters + public static Collection genTestSpecs() { - // Exception raising requires the Jython interpreter - interp = new PythonInterpreter(); + PyBufferTestSupport s = new PyBufferTestSupport(sliceLengths, sliceSteps); // Tests using local types of exporter - genWritable(new SimpleWritableExporter(abcMaterial.getBytes()), abcMaterial); - genReadonly(new SimpleExporter(byteMaterial.getBytes()), byteMaterial); - genReadonly(new StringExporter(stringMaterial.string), stringMaterial); - genWritable(new SimpleWritableExporter(emptyMaterial.getBytes()), emptyMaterial); + + ExporterFactory simpleExporter = new SimpleExporterFactory(); + s.add(simpleExporter, byteMaterial); + + ExporterFactory simpleWritableExporter = new WritableExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new SimpleWritableExporter(m.getBytes()); + } + + }; + s.add(simpleWritableExporter, abcMaterial); + s.add(simpleWritableExporter, emptyMaterial); + + ExporterFactory stringExporter = new ReadonlyExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new StringExporter(m.string); + } + + }; + s.add(stringExporter, stringMaterial); + + // Tests with an buffer implementation directly extending BaseBuffer + + ExporterFactory rollYourOwnExporter = new WritableExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new RollYourOwnExporter(m.getBytes()); + } + + }; + s.add(rollYourOwnExporter, byteMaterial); + s.add(rollYourOwnExporter, emptyMaterial); // Tests with PyByteArray - genWritable(new PyByteArray(abcMaterial.getBytes()), abcMaterial); - genWritable(new PyByteArray(longMaterial.getBytes()), longMaterial); - genWritable(new PyByteArray(), emptyMaterial); + + ExporterFactory pyByteArrayExporter = new WritableExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new PyByteArray(m.getBytes()); + } + + }; + s.add(pyByteArrayExporter, byteMaterial); + s.add(pyByteArrayExporter, longMaterial); + s.add(pyByteArrayExporter, emptyMaterial); // Tests with PyString - genReadonly(new PyString(abcMaterial.string), abcMaterial); - genReadonly(new PyString(), emptyMaterial); + + ExporterFactory pyStringExporter = new ReadonlyExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new PyString(m.string); + } + + }; + s.add(pyStringExporter, abcMaterial); + s.add(pyStringExporter, emptyMaterial); // Ensure case is tested where PyByteArray has an internal offset - PyByteArray truncated = new PyByteArray(stringMaterial.getBytes()); - truncated.delRange(0, 4); - ByteMaterial truncatedMaterial = new ByteMaterial(stringMaterial.string.substring(4)); - assert truncated.__alloc__() > truncatedMaterial.length; - genWritable(truncated, truncatedMaterial); + + ExporterFactory offsetPyByteArrayExporter = new WritableExporterFactory() { + + @Override + public BufferProtocol make(ByteMaterial m) { + // In this PyByteArray the data will not start at storage[0] + final int OFFSET = 4; + byte[] b = m.getBytes(); + // Make a copy with padding at the start and wrap it in a bytearray + byte[] data = new byte[OFFSET + b.length]; + System.arraycopy(b, 0, data, OFFSET, b.length); + PyByteArray a = new PyByteArray(data); + // This operation may (will) be implemented without data movement + a.delRange(0, OFFSET); + assert a.__alloc__() > b.length; + return a; + } + + }; + s.add(offsetPyByteArrayExporter, byteMaterial); + s.add(offsetPyByteArrayExporter, longMaterial); + + // Return the generated test data + + List ret = s.getTestData(); + if (PRINT_KEY) { + int key = 0; + for (TestSpec[] r : ret) { + TestSpec spec = r[0]; + System.out.printf("%6d : %s\n", key++, spec.toString()); + } + } + return ret; } - /** Generate a series of test material for a writable object. */ - private void genWritable(BufferProtocol exporter, ByteMaterial material) { - generate(exporter, material, false); + /** + * Brevity allowing each test to announce itself by naming the part of the api tested. + * + * @param api naming the part of the api tested + */ + protected void announce(String api) { + if (verbosity > 0) { + System.out.printf("%-30s %s\n", api + ":", spec.toString()); + } } - /** Generate a series of test material for a read-only object. */ - private void genReadonly(BufferProtocol exporter, ByteMaterial material) { - generate(exporter, material, true); + /** Test method for {@link org.python.core.PyBUF#isReadonly()}. */ + @Test + public void testIsReadonly() { + announce("isReadonly"); + assertTrue(view.isReadonly() == spec.readonly); } - /** Lengths we will use if we can when slicing view */ - private static final int[] sliceLengths = {1, 2, 5, 0, LONG / 4}; + /** Test method for {@link org.python.core.PyBUF#getNdim()}. */ + @Test + public void testGetNdim() { + announce("getNdim"); + // Only know how to do 1 dimension at the moment + assertEquals("unexpected ndim", spec.shape.length, view.getNdim()); + } - /** Step sizes we will use if we can when slicing view */ - private static final int[] sliceSteps = {1, 2, 3, 7}; + /** Test method for {@link org.python.core.PyBUF#getShape()}. */ + @Test + public void testGetShape() { + announce("getShape"); + int[] shape = view.getShape(); + assertNotNull("shape[] should always be provided", shape); + assertIntsEqual("unexpected shape", spec.shape, shape); + } - /** - * Generate a series of test material for a read-only or writable object. Given one exporter, - * and its reference ByteMaterial this method first queues a BufferTestPair corresponding to the - * exporter as the test subject and its test material. This provides a "direct" PyBuffer view on - * the exporter. It then goes on to make a variety of sliced PyBuffer views of the exporter by - * calling {@link PyBuffer#getBufferSlice(int, int, int, int)} on the direct view. The slices - * are made with a variety of argument combinations, filtered down to those that make sense for - * the size of the direct view. Each sliced buffer (considered a test subject now), together - * with correspondingly sliced reference ByteMaterial is queued as BufferTestPair. - * - * @param exporter underlying object - * @param material reference material corresponding to the exporter - * @param readonly whether the exporter is of read-only type - */ - private void generate(BufferProtocol exporter, ByteMaterial material, boolean readonly) { + /** Test method for {@link org.python.core.PyBUF#getLen()}. */ + @Test + public void testGetLen() { + announce("getLen"); + assertEquals("unexpected length", ref.length, view.getLen()); + } - // Generate a test using the buffer directly exported by the exporter - PyBuffer direct = queue(exporter, material, readonly); + /** Test method for {@link org.python.core.PyBUF#getObj()}. */ + @Test + public void testGetObj() { + announce("getObj"); + assertEquals("unexpected exporting object", obj, view.getObj()); + } - // Generate some slices from the material and this direct view - int N = material.length; - int M = (N + 4) / 4; // At least one and about N/4 + /** Test method for {@link org.python.core.PyBuffer#byteAt(int)}. */ + @Test + public void testByteAt() { + announce("byteAt"); + for (int i = 0; i < ref.length; i++) { + assertEquals(ref.bytes[i], view.byteAt(i)); + } + } - // For a range of start positions up to one beyond the end - for (int start = 0; start <= N; start += M) { - // For a range of lengths - for (int length : sliceLengths) { + /** Test method for {@link org.python.core.PyBuffer#byteAt(int[])}. */ + @Test + public void testByteAtNdim() { + announce("byteAt (n-dim)"); + int[] index = new int[1]; - if (length == 0) { - queue(direct, material, start, 0, 1, readonly); - queue(direct, material, start, 0, 2, readonly); + if (view.getShape().length != 1) { + fail("Test not implemented if dimensions != 1"); + } + // Run through 1D index for view + for (int i = 0; i < ref.length; i++) { + index[0] = i; + assertEquals(ref.bytes[i], view.byteAt(index)); + } - } else if (length == 1 && start < N) { - queue(direct, material, start, 1, 1, readonly); - queue(direct, material, start, 1, 2, readonly); + // Check 2D index throws + try { + view.byteAt(0, 0); + fail("Use of 2D index did not raise exception"); + } catch (PyException pye) { + // Expect BufferError + assertEquals(Py.BufferError, pye.type); + } + } - } else if (start < N) { + /** Test method for {@link org.python.core.PyBuffer#intAt(int)}. */ + @Test + public void testIntAt() { + announce("intAt"); + for (int i = 0; i < ref.length; i++) { + assertEquals(ref.ints[i], view.intAt(i)); + } + } - // And for a range of step sizes - for (int step : sliceSteps) { - // Check this is a feasible slice - if (start + (length - 1) * step < N) { - queue(direct, material, start, length, step, readonly); - } + /** Test method for {@link org.python.core.PyBuffer#intAt(int[])}. */ + @Test + public void testIntAtNdim() { + announce("intAt (n-dim)"); + int[] index = new int[1]; + + if (view.getShape().length != 1) { + fail("Test not implemented for dimensions != 1"); + } + // Run through 1D index for view + for (int i = 0; i < ref.length; i++) { + index[0] = i; + assertEquals(ref.ints[i], view.intAt(index)); + } + // Check 2D index throws + try { + view.intAt(0, 0); + fail("Use of 2D index did not raise exception"); + } catch (PyException pye) { + // Expect BufferError + assertEquals(Py.BufferError, pye.type); + } + } + + /** Test method for {@link org.python.core.PyBuffer#storeAt(byte, int)}. */ + @Test + public void testStoreAt() { + announce("storeAt"); + int n = ref.length; + int[] exp = ref.ints.clone(); + if (!spec.readonly) { + // Write modified test material into each location using storeAt() + for (int i = 0; i < n; i++) { + byte v = (byte)(exp[i] ^ 3); // twiddle some bits + view.storeAt(v, i); + } + // Compare each location with modified test data using intAt() + for (int i = 0; i < n; i++) { + assertEquals(exp[i] ^ 3, view.intAt(i)); + } + } else { + // Write should throw + for (int i = 0; i < n; i++) { + try { + view.storeAt((byte)3, i); + fail("Write access not prevented: " + spec); + } catch (PyException pye) { + // Expect TypeError (not BufferError which getBuffer can raise) + assertEquals(Py.TypeError, pye.type); + } + } + } + } + + /** Test method for {@link org.python.core.PyBuffer#storeAt(byte, int[])}. */ + @Test + public void testStoreAtNdim() { + announce("storeAt (n-dim)"); + int[] index = new int[1]; + int n = ref.length; + int[] exp = ref.ints.clone(); + if (!spec.readonly) { + // Write modified test material into each location using storeAt() + for (int i = 0; i < n; i++) { + index[0] = i; + byte v = (byte)(exp[i] ^ 3); // twiddle some bits + view.storeAt(v, index); + } + // Compare each location with modified test data using intAt() + for (int i = 0; i < n; i++) { + index[0] = i; + assertEquals(exp[i] ^ 3, view.intAt(index)); + } + if (spec.shape.length == 1) { + // Check 2D index throws + try { + view.storeAt((byte)1, 0, 0); + fail("Use of 2D index did not raise exception"); + } catch (PyException pye) { + // Expect BufferError + // XXX ... but should it be TypeError here? + assertEquals(Py.BufferError, pye.type); + } + } + } else { + // Write should throw + for (int i = 0; i < n; i++) { + index[0] = i; + try { + view.storeAt((byte)3, index); + fail("Write access not prevented: " + spec); + } catch (PyException pye) { + // Expect TypeError (not BufferError which getBuffer can raise) + assertEquals(Py.TypeError, pye.type); + } + } + } + } + + /** Test method for {@link org.python.core.PyBuffer#copyTo(byte[], int)}. */ + @Test + public void testCopyTo() { + final int OFFSET = 5; + announce("copyTo"); + int n = ref.length; + // Try with zero offset + byte[] actual = new byte[n]; + view.copyTo(actual, 0); + ByteBufferTestSupport.assertBytesEqual("copyTo() incorrect", ref.bytes, actual, 0); + // Try to middle of array + actual = new byte[n + 2 * OFFSET]; + view.copyTo(actual, OFFSET); + ByteBufferTestSupport.assertBytesEqual("copyTo(offset) incorrect", ref.bytes, actual, + OFFSET); + assertEquals("data before destination", 0, actual[OFFSET - 1]); + assertEquals("data after destination", 0, actual[OFFSET + n]); + } + + /** Test method for {@link org.python.core.PyBuffer#copyTo(int, byte[], int, int)}. */ + @Test + public void testSliceCopyTo() { + announce("copyTo (from slice)"); + final int OFFSET = 3; + + int n = ref.length; + byte[] before = new byte[n + 2 * OFFSET]; + final byte BLANK = 7; + Arrays.fill(before, BLANK); + + // Try destination positions in actual[] of 0 and OFFSET + for (int destPos = 0; destPos <= OFFSET; destPos += OFFSET) { + // Try source positions in 0 and OFFSET + for (int srcIndex = 0; srcIndex <= OFFSET; srcIndex += OFFSET) { + + // A variety of lengths from zero to (n-srcIndex)-ish + for (int length = 0; srcIndex + length <= n; length = 2 * length + 1) { + doTestSliceCopyTo(srcIndex, before, destPos, length, n); + } + + // And from exactly n-srcIndex down to zero-ish + for (int trim = 0; srcIndex + trim <= n; trim = 2 * trim + 1) { + int length = n - srcIndex - trim; + doTestSliceCopyTo(srcIndex, before, destPos, length, n); + } + } + } + } + + /** Helper function for {@link #testSliceCopyTo()} */ + private void doTestSliceCopyTo(int srcIndex, byte[] before, int destPos, int length, int n) { + + if (verbosity > 1) { + System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", srcIndex, srcIndex + + length, n, destPos, destPos + length, before.length); + } + + // Test the method + byte[] dest = before.clone(); + view.copyTo(srcIndex, dest, destPos, length); + + // Check the write to dest contains a correctly positioned copy of the view (=ref.bytes) + byte[] viewBytes = PyBufferTestSupport.bytesFromByteAt(view); + ByteBufferTestSupport.checkReadCorrect(ref.bytes, viewBytes, dest, destPos, length, 1, + srcIndex, 1); + + } + + /** Test method for {@link org.python.core.PyBuffer#copyFrom(byte[], int, int, int)}. */ + @Test + public void testCopyFrom() { + announce("copyFrom"); + final int OFFSET = 3; + final int L = ref.length; + + // Make some source material to copy from (longer since need to test at OFFSET too). + byte[] src = (new ByteMaterial(48, ref.length + OFFSET, 1)).bytes; + + // Our test is against the underlying object of which the view may be a slice + TestSpec underlying = spec.getOriginal(); + int start = spec.getStart(); + int stride = spec.getStride(); + + // Try source positions in 0 and OFFSET + for (int srcPos = 0; srcPos <= OFFSET; srcPos += OFFSET) { + + // Try destination positions in test object of 0 and OFFSET + for (int destIndex = 0; destIndex <= OFFSET; destIndex += OFFSET) { + + // A variety of lengths from zero to (n-destIndex)-ish + for (int length = 0; destIndex + length <= L; length = 2 * length + 1) { + doTestCopyFrom(src, srcPos, underlying, start, length, stride, destIndex); + } + + // And from exactly n-destIndex down to zero-ish + for (int trim = 0; destIndex + trim <= L; trim = 2 * trim + 1) { + int length = ref.length - destIndex - trim; + doTestCopyFrom(src, srcPos, underlying, start, length, stride, destIndex); + } + } + } + } + + /** Helper function for {@link #testCopyFrom()} */ + private void doTestCopyFrom(byte[] src, int srcPos, TestSpec underlying, int start, int length, + int stride, int destIndex) { + + if (verbosity > 1) { + System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d]\n", srcPos, srcPos + length, + ref.length, destIndex, destIndex + length); + } + + // Initialise the object (have to do each time) + createObjAndView(); + PyBuffer underlyingView = obj.getBuffer(underlying.flags & ~PyBUF.WRITABLE); + byte[] before = bytesFromByteAt(underlyingView); + + if (!spec.readonly) { + // This is the call we are testing (a write operation). + view.copyFrom(src, srcPos, destIndex, length); + + // Our test is against the underlying object of which the view may be a slice + byte[] after = bytesFromByteAt(underlyingView); + int underlyingDestIndex = start + destIndex * stride; + + // Test that the corresponding bytes of the underlying object match data copied in + ByteBufferTestSupport.checkWriteCorrect(before, after, src, srcPos, length, 1, + underlyingDestIndex, stride); + + } else { + // Should fail (write operation) + try { + view.copyFrom(src, srcPos, destIndex, length); + fail("Write access not prevented: " + spec); + } catch (PyException pye) { + // Expect TypeError only if the buffer was readonly + assertEquals(Py.TypeError, pye.type); + } + } + } + + /** Test method for {@link org.python.core.PyBuffer#copyFrom(PyBuffer)}. */ + @Test + public void testCopyFromPyBuffer() { + announce("copyFrom (PyBuffer)"); + + /* + * The test material (this time) presents a view that has n items of size i, that are spaced + * in the underlying buffer with stride s. + */ + final int n = spec.ref.length; + final int p = spec.getStride(); + + // The material we copy to it should have these strides: + int[] srcStrides; + if (n < 2) { + srcStrides = new int[] {1}; + } else if (p > 2 || p < -2) { + srcStrides = new int[] {1, p - 1, p, p + 1, -p + 1, -p, -p - 1}; + } else if (p == 2 || p == -2) { + srcStrides = new int[] {1, 2, 3, -1, -2, -3}; + } else { // ( s==1 || s==-1 ) + srcStrides = new int[] {1, 2, -1, -2}; + } + + // Also need the maximum absolute value + int maxStride = 0; + for (int stride : srcStrides) { + if (stride > maxStride) { + maxStride = stride; + } else if (-stride > maxStride) { + maxStride = -stride; + } + } + + // And these offsets to the lowest-indexed source item + int maxOffset = n + 1; + int[] srcOffsets = new int[] {0, (maxOffset + 1) / 3, maxOffset}; + + // Make the source material to copy from, big enough to accommodate n strides + int srcMaterialSize = n * maxStride + maxOffset; + ByteMaterial srcMaterial = new ByteMaterial(48, srcMaterialSize, 1); + + /* + * Now we need a series of PyBuffer views on the source data, sliced and offset according to + * the offset and stride values we have enumerated. We'd like to use the same factory as the + * current test view (this.view), because copy from its own type might be optimised, and a + * different, bog-standard factory to test the general case. + */ + ExporterFactory[] factories = {spec.factory, new SimpleExporterFactory()}; + + for (ExporterFactory factory : factories) { + /* + * We'll use the same apparatus to create the source buffer as we use to make the test + * cases. The specifications for them will all be derived from this one: + */ + TestSpec original = new TestSpec(factory, srcMaterial); + /* + * Do this where the pattern of indices constituting src overlaps (or not) the pattern + * of view in challenging ways, including greater and smaller strides. + */ + for (int stride : srcStrides) { + for (int offset : srcOffsets) { + int start = (stride > 0) ? offset : srcMaterialSize - offset - 1; + doTestCopyFrom(original, start, n, stride); + } + } + } + + } + + /** Helper function for {@link #testCopyFromPyBuffer()} */ + private void doTestCopyFrom(TestSpec original, int start, int n, int stride) { + + // Derive sliced test material from the original + TestSpec srcSpec = new SlicedTestSpec(original, 1, start, n, stride); + ObjectAndView pair = srcSpec.makePair(); + PyBuffer src = pair.view; + byte[] srcBytes = srcSpec.ref.bytes; + + // And for the test object + int s = spec.getStart(); + int p = spec.getStride(); + String srcName = pair.obj.getClass().getSimpleName(); + if (verbosity > 1) { + int end = start + (n - 1) * stride + (stride > 0 ? 1 : -1); + int e = s + (n - 1) * p + (p > 0 ? 1 : -1); + System.out.printf(" copy from src[%d:%d:%d] %s(%d) to obj[%d:%d:%d]\n", // + start, end, stride, srcName, n, // + s, e, p); + } + + // Initialise the destination object and view (have to do each time) from spec + createObjAndView(); + + // Our test is against the underlying object of which the view may be a slice + TestSpec underlying = spec.getOriginal(); + PyBuffer underlyingView = obj.getBuffer(underlying.flags & ~PyBUF.WRITABLE); + byte[] before = bytesFromByteAt(underlyingView); + + if (!spec.readonly) { + // This is the call we are testing (a write operation). + view.copyFrom(src); + + // Our test is against the underlying object of which the view may be a slice + byte[] after = bytesFromByteAt(underlyingView); + + // Test that the corresponding bytes of the underlying object match data copied in + ByteBufferTestSupport.checkWriteCorrect(before, after, srcBytes, 0, n, 1, s, p); + + } else { + // Should fail (write operation) + try { + view.copyFrom(src); + fail("Write access not prevented: " + spec); + } catch (PyException pye) { + // Expect TypeError only if the buffer was readonly + assertEquals(Py.TypeError, pye.type); + } + } + } + + /** Test method for {@link org.python.core.PyBuffer#copyFrom(PyBuffer)} when source is same. */ + @Test + public void testCopyFromSelf() { + announce("copyFrom (self)"); + + // The test material (this time) presents a view of n bytes from a buffer of L bytes. + final int n = ref.length; + TestSpec original = spec.getOriginal(); + if (spec.readonly || spec == original || n < 1) { + // We're only testing with sliced writable views + return; + } + final int p = spec.getStride(); + final int L = original.ref.length; + + /* + * We want to make another sliced view on the same test object, with the same number of + * items n, but different stride and/or offset. Strides above, equal to and below (if + * possible) the destination stride are of interest. + */ + int[] srcStrides; + if (n < 2) { + srcStrides = new int[] {1}; + } else if (p > 2 || p < -2) { + srcStrides = new int[] {1, p - 1, p, p + 1, -p + 1, -p, -p - 1}; + } else if (p == 2 || p == -2) { + srcStrides = new int[] {1, 2, 3, -1, -2, -3}; + } else { // ( p==1 || p==-1 ) + srcStrides = new int[] {1, 2, -1, -2}; + } + + for (int srcStride : srcStrides) { + int absStride; + if (srcStride > 0) { + absStride = srcStride; + /* + * Compute the highest start index such that we can fit n items spaced at absStride + * into the buffer before reaching the end. + */ + int maxOffset = L - 1 - absStride * (n - 1); + // There might not be such an start. If there is, we can do one or more tests. + if (maxOffset >= 0) { + // A positive-stepping slice could fit, for some start positions + int incOffset = 1 + maxOffset / 4; + for (int srcOffset = 0; srcOffset <= maxOffset; srcOffset += incOffset) { + doTestCopyFromSelf(srcOffset, srcStride, n); } - - // Now use all the step sizes negatively - for (int step : sliceSteps) { - // Check this is a feasible slice - if (start - (length - 1) * step >= 0) { - queue(direct, material, start, length, -step, readonly); - } + } + } else {// srcStride < 0 + absStride = -srcStride; + /* + * Compute the lowest start index such that we can fit n items spaced at absStride + * into the buffer before reaching the beginning. + */ + int minOffset = absStride * (n - 1) + 1; + // There might not be such an start. If there is, we can do one or more tests. + if (minOffset < L) { + // A negative-stepping slice could fit, for some start positions + int incOffset = 1 + (L - 1 - minOffset) / 4; + for (int srcOffset = L - 1; srcOffset > minOffset; srcOffset -= incOffset) { + doTestCopyFromSelf(srcOffset, srcStride, n); } } } } } - /** Generate and queue one test of non-slice type (if getting a buffer succeeds). */ - private PyBuffer queue(BufferProtocol exporter, ByteMaterial material, boolean readonly) { - if (verbosity > 2) { - System.out.printf("queue non-slice: length=%d, readonly=%s\n", material.length, - readonly); + /** Helper function for {@link #testCopyFromPyBuffer()} */ + private void doTestCopyFromSelf(int srcStart, int srcStride, int n) { + + // Initialise the destination object and view (have to do each time) from spec + createObjAndView(); + + // Report the slice of the test object we are writing + int dstStart = spec.getStart(); + int dstStride = spec.getStride(); + String srcName = obj.getClass().getSimpleName(); + if (verbosity > 1) { + int srcEnd = srcStart + (n - 1) * srcStride + (srcStride > 0 ? 1 : -1); + int dstEnd = dstStart + (n - 1) * dstStride + (dstStride > 0 ? 1 : -1); + System.out.printf(" copy from obj[%d:%d:%d] %s(%d) to obj[%d:%d:%d]\n", // + srcStart, srcEnd, srcStride, srcName, n, // + dstStart, dstEnd, dstStride); } - BufferTestPair pair = new BufferTestPair(exporter, material, readonly); - queue(pair); - return pair.view; - } + assert !spec.readonly; // Test is only called if writable - /** Generate and queue one test of slice type (if getting a buffer succeeds). */ - private PyBuffer queue(PyBuffer direct, ByteMaterial material, int start, int length, int step, - boolean readonly) { + // Our test is against the underlying object of which the view may be a slice + try (PyBuffer underlying = obj.getBuffer(PyBUF.FULL_RO)) { - int flags = readonly ? PyBUF.FULL_RO : PyBUF.FULL; - PyBuffer subject = null; + // Take a snapshot before the call + byte[] before = bytesFromByteAt(underlying); - /* - * Make a slice. We ignore this case if we fail, because we are not testing slice creation - * here, but making slices to be tested as buffers. We'll test slice creation in - * testGetBufferSlice. - */ - try { - if (verbosity > 2) { - System.out.printf(" queue slice: start=%4d, length=%4d, step=%4d\n", start, - length, step); - } - subject = direct.getBufferSlice(flags, start, length, step); - ByteMaterial sliceMaterial = material.slice(start, length, step); - BufferTestPair pair = new BufferTestPair(subject, sliceMaterial, step, readonly); - queue(pair); - } catch (Exception e) { - /* - * We ignore this case if we fail, because we are not testing slice creation here, but - * making slices to be tested as buffers. We'll test slice creation elsewhere. - */ - if (verbosity > 2) { - System.out.printf("*** SKIP %s\n", e); - } - } + // Take the required slice-view to use as the source. + PyBuffer src = underlying.getBufferSlice(PyBUF.FULL_RO, srcStart, n, srcStride); + byte[] srcBytes = bytesFromByteAt(src); - return subject; - } + // This is the call we are testing (a write operation). + view.copyFrom(src); - /** Queue one instance of test material for a read-only or writable object. */ - private void queue(BufferTestPair pair) { - buffersToRead.add(pair); - if (pair.readonly) { - buffersToFailToWrite.add(pair); - } else { - buffersToWrite.add(pair); - } - } - - /** Read operations should succeed on all these objects. */ - private List buffersToRead = new LinkedList(); - /** Write operations should succeed on all these objects. */ - private List buffersToWrite = new LinkedList(); - /** Write operations should fail on all these objects. */ - private List buffersToFailToWrite = new LinkedList(); - - /** - * A one-dimensional exporter should be able to give us a buffer for all these flag types. - */ - private static final int[] simpleFlags = {PyBUF.SIMPLE, PyBUF.ND, PyBUF.STRIDES, - PyBUF.INDIRECT, PyBUF.FULL_RO}; - - /** To {@link #simpleFlags} we can add any of these */ - private static final int[] simpleTassles = {0, PyBUF.FORMAT, PyBUF.C_CONTIGUOUS, - PyBUF.F_CONTIGUOUS, PyBUF.ANY_CONTIGUOUS}; - - /** - * A one-dimensional exporter with stride!=1 is restricted to give us a buffer only for these - * flag types. - */ - private static final int[] strided1DFlags = {PyBUF.STRIDES, PyBUF.INDIRECT, PyBUF.FULL_RO}; - - /** To {@link #strided1DFlags} we can add any of these */ - private static final int[] strided1DTassles = {0, PyBUF.FORMAT}; - - /** - * Test method for {@link org.python.core.PyBUF#isReadonly()}. - */ - public void testIsReadonly() { - - for (BufferTestPair test : buffersToWrite) { - if (verbosity > 0) { - System.out.println("isReadonly: " + test); - } - assertFalse(test.view.isReadonly()); - } - - for (BufferTestPair test : buffersToFailToWrite) { - if (verbosity > 0) { - System.out.println("isReadonly: " + test); - } - assertTrue(test.view.isReadonly()); + // Test that the corresponding bytes of the underlying object match data copied in + byte[] after = bytesFromByteAt(underlying); + ByteBufferTestSupport.checkWriteCorrect(before, after, srcBytes, 0, n, 1, dstStart, + dstStride); } } /** - * Test method for {@link org.python.core.PyBUF#getNdim()}. + * Test method for {@link org.python.core.BufferProtocol#getBuffer()} and + * {@link org.python.core.PyBuffer#getBuffer()}. */ - public void testGetNdim() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getNdim: " + test); - } - assertEquals("unexpected ndim", test.shape.length, test.view.getNdim()); - } - } - - /** - * Test method for {@link org.python.core.PyBUF#getShape()}. - */ - public void testGetShape() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getShape: " + test); - } - int[] shape = test.view.getShape(); - assertNotNull("shape[] should always be provided", shape); - assertIntsEqual("unexpected shape", test.shape, shape); - } - } - - /** - * Test method for {@link org.python.core.PyBUF#getLen()}. - */ - public void testGetLen() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getLen: " + test); - } - assertEquals("unexpected length", test.material.length, test.view.getLen()); - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#byteAt(int)}. - */ - public void testByteAt() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("byteAt: " + test); - } - int n = test.material.length; - byte[] exp = test.material.bytes; - for (int i = 0; i < n; i++) { - assertEquals(exp[i], test.view.byteAt(i)); - } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#byteAt(int[])}. - */ - public void testByteAtNdim() { - int[] index = new int[1]; - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("byteAt(array): " + test); - } - if (test.view.getShape().length != 1) { - fail("Test not implemented if dimensions != 1"); - } - byte[] exp = test.material.bytes; - int n = test.material.length; - // Run through 1D index for view - for (int i = 0; i < n; i++) { - index[0] = i; - assertEquals(exp[i], test.view.byteAt(index)); - } - - // Check 2D index throws - try { - test.view.byteAt(0, 0); - fail("Use of 2D index did not raise exception"); - } catch (PyException pye) { - // Expect BufferError - assertEquals(Py.BufferError, pye.type); - } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#intAt(int)}. - */ - public void testIntAt() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("intAt: " + test); - } - int n = test.material.length; - int[] exp = test.material.ints; - for (int i = 0; i < n; i++) { - assertEquals(exp[i], test.view.intAt(i)); - } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#intAt(int[])}. - */ - public void testIntAtNdim() { - int[] index = new int[1]; - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("intAt(array): " + test); - } - if (test.view.getShape().length != 1) { - fail("Test not implemented for dimensions != 1"); - } - int[] exp = test.material.ints; - int n = test.material.length; - // Run through 1D index for view - for (int i = 0; i < n; i++) { - index[0] = i; - assertEquals(exp[i], test.view.intAt(index)); - } - // Check 2D index throws - try { - test.view.intAt(0, 0); - fail("Use of 2D index did not raise exception"); - } catch (PyException pye) { - // Expect BufferError - assertEquals(Py.BufferError, pye.type); - } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#storeAt(byte, int)}. - */ - public void testStoreAt() { - for (BufferTestPair test : buffersToWrite) { - if (verbosity > 0) { - System.out.println("storeAt: " + test); - } - int n = test.material.length; - int[] exp = test.material.ints; - // Write modified test material into each location using storeAt() - for (int i = 0; i < n; i++) { - byte v = (byte)(exp[i] ^ 3); // twiddle some bits - test.view.storeAt(v, i); - } - // Compare each location with modified test data using intAt() - for (int i = 0; i < n; i++) { - assertEquals(exp[i] ^ 3, test.view.intAt(i)); - } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#storeAt(byte, int[])}. - */ - public void testStoreAtNdim() { - for (BufferTestPair test : buffersToWrite) { - if (verbosity > 0) { - System.out.println("storeAt: " + test); - } - int n = test.material.length; - int[] exp = test.material.ints; - // Write modified test material into each location using storeAt() - for (int i = 0; i < n; i++) { - byte v = (byte)(exp[i] ^ 3); // twiddle some bits - test.view.storeAt(v, i); - } - // Compare each location with modified test data using intAt() - for (int i = 0; i < n; i++) { - assertEquals(exp[i] ^ 3, test.view.intAt(i)); - } - // Check 2D index throws - try { - test.view.storeAt((byte)1, 0, 0); - fail("Use of 2D index did not raise exception"); - } catch (PyException pye) { - // Expect BufferError - assertEquals(Py.BufferError, pye.type); - } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#copyTo(byte[], int)}. - */ - public void testCopyTo() { - final int OFFSET = 5; - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("copyTo: " + test); - } - int n = test.material.length; - // Try with zero offset - byte[] actual = new byte[n]; - test.view.copyTo(actual, 0); - assertBytesEqual("copyTo() incorrect", test.material.bytes, actual, 0); - // Try to middle of array - actual = new byte[n + 2 * OFFSET]; - test.view.copyTo(actual, OFFSET); - assertBytesEqual("copyTo(offset) incorrect", test.material.bytes, actual, OFFSET); - assertEquals("data before destination", 0, actual[OFFSET - 1]); - assertEquals("data after destination", 0, actual[OFFSET + n]); - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#copyTo(int, byte[], int, int)}. - */ - public void testSliceCopyTo() { - final int OFFSET = 5; - final byte BLANK = 7; - - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("copyTo(from slice): " + test); - } - PyBuffer view = test.view; - - int n = test.material.length; - byte[] actual = new byte[n + 2 * OFFSET]; - - // Try destination positions in actual[] of 0 and OFFSET - for (int destPos = 0; destPos <= OFFSET; destPos += OFFSET) { - // Try source positions in 0 and OFFSET - for (int srcIndex = 0; srcIndex <= OFFSET; srcIndex += OFFSET) { - - // A variety of lengths from zero to (n-srcIndex)-ish - for (int length = 0; srcIndex + length <= n; length = 2 * length + 1) { - - if (verbosity > 1) { - System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", - srcIndex, srcIndex + length, n, destPos, destPos + length, - actual.length); - } - - Arrays.fill(actual, BLANK); - - // Test the method - view.copyTo(srcIndex, actual, destPos, length); - - // Check changed part of destination - assertBytesEqual("copyTo(slice) incorrect", test.material.bytes, srcIndex, - length, actual, destPos); - if (destPos > 0) { - assertEquals("data before destination", BLANK, actual[destPos - 1]); - } - assertEquals("data after destination", BLANK, actual[destPos + length]); - } - - // And from exactly n-srcIndex down to zero-ish - for (int trim = 0; srcIndex + trim <= n; trim = 2 * trim + 1) { - int length = n - srcIndex - trim; - - if (verbosity > 1) { - System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", - srcIndex, srcIndex + length, n, destPos, destPos + length, - actual.length); - } - - Arrays.fill(actual, BLANK); - - // Test the method - view.copyTo(srcIndex, actual, destPos, length); - - // Check changed part of destination - assertBytesEqual("copyTo(slice) incorrect", test.material.bytes, srcIndex, - length, actual, destPos); - if (destPos > 0) { - assertEquals("data before destination", BLANK, actual[destPos - 1]); - } - assertEquals("data after destination", BLANK, actual[destPos + length]); - } - } - } - } - } - - /** - * Test method for {@link org.python.core.PyBuffer#copyFrom(byte[], int, int, int)}. - */ - public void testCopyFrom() { - final int OFFSET = 5; - final byte BLANK = 7; - - for (BufferTestPair test : buffersToWrite) { - if (verbosity > 0) { - System.out.println("copyFrom(): " + test); - } - PyBuffer view = test.view; - - int n = test.material.length; - byte[] actual = new byte[n]; - byte[] expected = new byte[n]; - - // Make some source material for copies (need to test at OFFSET too). - byte[] src = new byte[n + OFFSET]; - for (int i = 0; i < src.length; i++) { - src[i] = (byte)i; - } - - // Try destination positions in test object of 0 and OFFSET - for (int destIndex = 0; destIndex <= OFFSET; destIndex += OFFSET) { - - // Try source positions in 0 and OFFSET - for (int srcPos = 0; srcPos <= OFFSET; srcPos += OFFSET) { - - // A variety of lengths from zero to (n-destIndex)-ish - for (int length = 0; destIndex + length <= n; length = 2 * length + 1) { - - if (verbosity > 1) { - System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", - srcPos, srcPos + length, n, destIndex, destIndex + length, - actual.length); - } - - // Initialise the object (have to do each time) and expected value - for (int i = 0; i < n; i++) { - expected[i] = BLANK; - view.storeAt(BLANK, i); - } - - // Test the method and extract the result to actual[] - view.copyFrom(src, srcPos, destIndex, length); - view.copyTo(actual, 0); - - // Complete what is should be in expected[] - for (int i = 0; i < length; i++) { - expected[destIndex + i] = src[srcPos + i]; - } - assertBytesEqual("copyFrom() incorrect", expected, actual, 0); - } - - // And from exactly n-destIndex down to zero-ish - for (int trim = 0; destIndex + trim <= n; trim = 2 * trim + 1) { - int length = n - destIndex - trim; - - if (verbosity > 1) { - System.out.printf(" copy src[%d:%d] (%d) to dst[%d:%d] (%d)\n", - srcPos, srcPos + length, n, destIndex, destIndex + length, - actual.length); - } - - // Initialise the object (have to do each time) and expected value - for (int i = 0; i < n; i++) { - expected[i] = BLANK; - view.storeAt(BLANK, i); - } - - // Test the method and extract the result to actual[] - view.copyFrom(src, srcPos, destIndex, length); - view.copyTo(actual, 0); - - // Complete what is should be in expected[] - for (int i = 0; i < length; i++) { - expected[destIndex + i] = src[srcPos + i]; - } - assertBytesEqual("copyFrom() incorrect", expected, actual, 0); - } - } + public void testGetBufferForRead() { + announce("getBuffer(READ): "); + // Go through all the allowed combinations of flags and tassles + for (int flags : spec.validFlags) { + for (int tassles : spec.validTassles) { + PyBuffer view2 = view.getBuffer(flags | tassles); + assertNotNull(view2); } } } @@ -639,161 +809,91 @@ * Test method for {@link org.python.core.BufferProtocol#getBuffer()} and * {@link org.python.core.PyBuffer#getBuffer()}. */ - public void testGetBuffer() { - - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getBuffer(): " + test); - } - for (int flags : test.validFlags) { - for (int tassle : test.validTassles) { - PyBuffer view = test.subject.getBuffer(flags | tassle); - assertNotNull(view); + public void testGetBufferForWrite() { + announce("getBuffer(WRITE): "); + if (!spec.readonly) { + // Go through all the allowed combinations of flags and tassles adding WRITABLE + for (int flags : spec.validFlags) { + for (int tassles : spec.validTassles) { + PyBuffer view2 = view.getBuffer(flags | tassles | PyBUF.WRITABLE); + assertNotNull(view2); } } - } - - for (BufferTestPair test : buffersToWrite) { - if (verbosity > 0) { - System.out.println("getBuffer(WRITABLE): " + test); - } - for (int flags : test.validFlags) { - for (int tassle : test.validTassles) { - PyBuffer view = test.subject.getBuffer(flags | tassle | PyBUF.WRITABLE); - assertNotNull(view); - } - } - } - - for (BufferTestPair test : buffersToFailToWrite) { - if (verbosity > 0) { - System.out.println("getBuffer(WRITABLE): " + test); - } - for (int flags : test.validFlags) { + } else { + // Go through all the allowed combinations of flags adding WRITABLE + for (int flags : spec.validFlags) { try { - test.subject.getBuffer(flags | PyBUF.WRITABLE); - fail("Write access not prevented: " + test); + view.getBuffer(flags | PyBUF.WRITABLE); + fail("Write access not prevented: " + spec); } catch (PyException pye) { // Expect BufferError assertEquals(Py.BufferError, pye.type); } } } - } /** * Test method for {@link org.python.core.PyBUF#release()}, exercising the release semantics of - * PyBuffer. + * PyBuffer in the try-with-resources pattern. */ - public void testRelease() { + @Test + public void testReleaseTryWithResources() { + announce("release (via try)"); + /* + * this.obj is an actual exporter and this.view is a buffer view onto it. + */ + int flags = PyBUF.STRIDES | PyBUF.FORMAT; - /* - * Testing the semantics of release() is tricky when it comes to 'final' release behaviour. - * We'd like to test that buffers can be acquired and released, that "over release" is - * detected as an error, and that after final release of the buffer (where the export count - * becomes zero) an exporter remains capable of exporting again. Each test is constructed - * with a subject and a view on the subject (if the subject is an exporter), so you might - * think the export count would be one in every case. Two problems: in many tests, the - * subject is a PyBuffer, which has the option (if it would work) to return itself; and a - * PyBuffer is not expected to provide a new buffer view once finally released. - */ - - Set uniqueBuffers = new HashSet(); - - // Test a balanced sequence of acquire and release using try-with-resources - for (BufferTestPair test : buffersToRead) { - doTestTryWithResources(test); + // The test setup should guarantee view is the only export + try (PyBuffer c = obj.getBuffer(flags)) { // = 2 exports + try (PyBuffer b = obj.getBuffer(PyBUF.FULL_RO); PyBuffer d = c.getBuffer(flags)) { + maybeCheckExporting(obj);// = 4 exports + } + maybeCheckExporting(obj); // = 2 exports + throw new Throwable("test"); + } catch (Throwable e) { + // Meh } - - // Now test a pattern of acquire and release with one more release than acquire - for (BufferTestPair test : buffersToRead) { - doTestRelease(test); - uniqueBuffers.add(test.view); - } - - // All buffers are released: test that any further release is detected as an error. - for (PyBuffer view : uniqueBuffers) { - doTestOverRelease(view); - } - - // All exporters are currently not exporting buffers - for (BufferTestPair test : buffersToRead) { - if (!(test.subject instanceof PyBuffer)) { - doTestGetAfterRelease(test); - } - } - + maybeCheckExporting(obj); // = 1 export + view.release(); + maybeCheckNotExporting(obj); // = 0 exports } /** - * Exercise try-with-resources on one BufferTestPair. + * Test method for {@link org.python.core.PyBUF#release()}, exercising release semantics in a + * sequence orchestrated by the client code. At the end, the view should be fully released, ( + * {@link PyBuffer#isReleased()}==true). */ - private void doTestTryWithResources(BufferTestPair test) { + @Test + public void testRelease() { + announce("release"); + int flags = PyBUF.STRIDES | PyBUF.FORMAT; - if (verbosity > 0) { - System.out.println("try with resources: " + test); - } - int flags = PyBUF.STRIDES | PyBUF.FORMAT; - BufferProtocol sub = test.subject; - - // The object will be exporting test.view and N other views we don't know about - try (PyBuffer c = sub.getBuffer(flags)) { // = N+1 exports - try (PyBuffer b = sub.getBuffer(PyBUF.FULL_RO); PyBuffer d =c.getBuffer(flags)) { - checkExporting(sub);// = N+3 exports - } - checkExporting(sub); // = N+1 exports - } - checkExporting(sub); // = N export - } - - /** - * Exercise the release semantics of one BufferTestPair. At the end, the view in the - * BufferTestPair should be fully released, ({@link PyBuffer#isReleased()}==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); + // The object will be exporting view only + PyBuffer a = view; // = 1 exports + PyBuffer b = obj.getBuffer(PyBUF.FULL_RO); // = 2 export + PyBuffer c = obj.getBuffer(flags); // = 3 exports + maybeCheckExporting(obj); // Now see that releasing in some other order works correctly - b.release(); // = N+2 exports - a.release(); // = N+1 exports - checkExporting(sub); + b.release(); // = 2 exports + a.release(); // = 1 exports + maybeCheckExporting(obj); // You can get a buffer from a buffer (c is unreleased) - PyBuffer d = c.getBuffer(flags); // = N+2 exports - c.release(); // = N+1 export - checkExporting(sub); - d.release(); // = N exports - } - - /** - * The view argument should be a fully released buffer, ({@link PyBuffer#isReleased()} - * ==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()); + PyBuffer d = c.getBuffer(flags); // = 2 exports + c.release(); // = 1 export + maybeCheckExporting(obj); + d.release(); // = no exports // Further releases are an error try { - view.release(); // = -1 exports (oops) + view.release(); // = -1 exports (oops) fail("excess release not detected"); } catch (Exception e) { // Success } - } /** @@ -802,41 +902,40 @@ * We check this is true, and that a new buffer may still be acquired from the real object, but * not from the released buffer. */ - private void doTestGetAfterRelease(BufferTestPair test) { + @Test + public void testGetAfterRelease() { + announce("getBuffer (after release)"); - if (verbosity > 0) { - System.out.println("get again: " + test); - } - BufferProtocol sub = test.subject; + // The test objects should have exactly one export + view.release(); - // Fail here if doTestRelease did not fully release, or - checkNotExporting(sub); + // The view can be checked, but not always the obj + maybeCheckNotExporting(obj); + maybeCheckNotExporting(view); // Further gets via the released buffer are an error try { - test.view.getBuffer(PyBUF.FULL_RO); + view.getBuffer(PyBUF.FULL_RO); fail("PyBuffer.getBuffer after final release not detected"); } catch (Exception e) { // Detected *and* prevented? - checkNotExporting(sub); + maybeCheckNotExporting(obj); } // And so are sliced gets try { - test.view.getBufferSlice(PyBUF.FULL_RO, 0, 0); + view.getBufferSlice(PyBUF.FULL_RO, 0, 0); fail("PyBuffer.getBufferSlice after final release not detected"); } catch (Exception e) { // Detected *and* prevented? - checkNotExporting(sub); + maybeCheckNotExporting(obj); } - /* - * Even after some abuse, we can still get and release a buffer. - */ - PyBuffer b = sub.getBuffer(PyBUF.FULL_RO); // = 1 export - checkExporting(sub); - b.release(); // = 0 exports - checkNotExporting(sub); + // Even after some abuse, we can still get and release a buffer. + PyBuffer b = obj.getBuffer(PyBUF.FULL_RO); // = 1 export + maybeCheckExporting(obj); + b.release(); // = 0 exports + maybeCheckNotExporting(obj); } /** @@ -845,7 +944,7 @@ * * @param subject */ - private void checkExporting(BufferProtocol subject) { + private void maybeCheckExporting(BufferProtocol subject) { if (subject instanceof TestableExporter) { assertTrue("exports not being counted", ((TestableExporter)subject).isExporting()); } else if (subject instanceof PyBuffer) { @@ -853,8 +952,8 @@ } else if (subject instanceof PyByteArray) { // Size-changing access should fail try { - ((PyByteArray)subject).bytearray_extend(Py.One); // Appends one zero byte - fail("bytearray_extend with exports should fail"); + ((PyByteArray)subject).bytearray_append(Py.One); // Appends one zero byte + fail("bytearray_append with exports should fail"); } catch (Exception e) { // Success } @@ -868,7 +967,7 @@ * * @param subject */ - private void checkNotExporting(BufferProtocol subject) { + private void maybeCheckNotExporting(BufferProtocol subject) { if (subject instanceof TestableExporter) { assertFalse("exports counted incorrectly", ((TestableExporter)subject).isExporting()); } else if (subject instanceof PyBuffer) { @@ -886,125 +985,123 @@ // Other types cannot be checked } - /** - * Test method for {@link org.python.core.PyBuffer#getBufferSlice(int, int, int, int)}. - */ + /** Test method for {@link org.python.core.PyBuffer#getBufferSlice(int, int, int, int)}. */ + @Test public void testGetBufferSliceWithStride() { + announce("getBuffer (slice & stride)"); - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getBufferSliceWithStride: " + test); - } - ByteMaterial material = test.material; - PyBuffer view = test.view; - boolean readonly = test.readonly; + // Generate some slices from the material and the test view + int N = ref.length; + int M = (N + 4) / 4; // At least one and about N/4 - // Generate some slices from the material and the test view - int N = material.length; - int M = (N + 4) / 4; // At least one and about N/4 + // For a range of start positions up to one beyond the end + for (int start = 0; start <= N; start += M) { + // For a range of lengths + for (int length : sliceLengths) { - // For a range of start positions up to one beyond the end - for (int start = 0; start <= N; start += M) { - // For a range of lengths - for (int length : sliceLengths) { + if (length == 0) { + doTestGetBufferSliceWithStride(start, 0, 1); + doTestGetBufferSliceWithStride(start, 0, 2); - if (length == 0) { - checkSlice(view, material, start, 0, 1, readonly); - checkSlice(view, material, start, 0, 2, readonly); + } else if (length == 1 && start < N) { + doTestGetBufferSliceWithStride(start, 1, 1); + doTestGetBufferSliceWithStride(start, 1, 2); - } else if (length == 1 && start < N) { - checkSlice(view, material, start, 1, 1, readonly); - checkSlice(view, material, start, 1, 2, readonly); + } else if (start < N) { - } else if (start < N) { + // And for a range of step sizes + for (int step : sliceSteps) { + // Check this is a feasible slice + if (start + (length - 1) * step < N) { + doTestGetBufferSliceWithStride(start, length, step); + } + } - // And for a range of step sizes - for (int step : sliceSteps) { - // Check this is a feasible slice - if (start + (length - 1) * step < N) { - checkSlice(view, material, start, length, step, readonly); - } - } - - // Now use all the step sizes negatively - for (int step : sliceSteps) { - // Check this is a feasible slice - if (start - (length - 1) * step >= 0) { - checkSlice(view, material, start, length, -step, readonly); - } + // Now use all the step sizes negatively + for (int step : sliceSteps) { + // Check this is a feasible slice + if (start - (length - 1) * step >= 0) { + doTestGetBufferSliceWithStride(start, length, -step); } } } } } - } /** * Helper for {@link #testGetBufferSliceWithStride()} that obtains one sliced buffer to * specification and checks it against the material. */ - private void checkSlice(PyBuffer view, ByteMaterial material, int start, int length, int step, - boolean readonly) { + private void doTestGetBufferSliceWithStride(int first, int count, int step) { - int flags = readonly ? PyBUF.FULL_RO : PyBUF.FULL; + // view is a view matching ref.bytes. Make a reference value for a further slice. + TestSpec slicedSpec = new SlicedTestSpec(spec, spec.getItemsize(), first, count, step); if (verbosity > 1) { - System.out.printf(" checkSlice: start=%4d, length=%4d, step=%4d \n", start, length, - step); + System.out.printf( + " slice first=%4d, count=%4d, step=%4d -> underlying start=%4d, stride=%4d\n", + first, count, step, slicedSpec.getStart(), slicedSpec.getStride()); } - byte[] expected = sliceBytes(material.bytes, start, length, step); - PyBuffer sliceView = view.getBufferSlice(flags, start, length, step); - byte[] result = bytesFromByteAt(sliceView); - assertBytesEqual(" testGetBufferSliceWithStride failure: ", expected, result); + // Now compute that further slice using the library under test (not makePair) + PyBuffer slicedView = view.getBufferSlice(spec.flags, first, count, step); + byte[] slice = PyBufferTestSupport.bytesFromByteAt(slicedView); + + // Did we get the same as the reference material in the + ByteBufferTestSupport.assertBytesEqual("slice incorrect", slicedSpec.ref.bytes, slice); } - /** - * Test method for {@link org.python.core.PyBuffer#getBuf()}. - */ - public void testGetBuf() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getBuf: " + test); - } - int stride = test.strides[0]; + /** Test method for {@link org.python.core.PyBuffer#getNIOByteBuffer()}. */ + @Test + public void testGetNIOByteBuffer() { + announce("getNIOByteBuffer"); + int stride = spec.getStride(); + ByteBuffer bb = view.getNIOByteBuffer(); + ByteBufferTestSupport.assertBytesEqual("buffer does not match reference", ref.bytes, bb, + stride); + if (spec.readonly) { + assertTrue("ByteBuffer should be read-only", bb.isReadOnly()); + } else { + assertFalse("ByteBuffer should not be read-only", bb.isReadOnly()); + } - if (stride == 1) { + } - // The client should not have to support navigation with the strides array - int flags = test.readonly ? PyBUF.SIMPLE : PyBUF.SIMPLE + PyBUF.WRITABLE; - PyBuffer view = test.subject.getBuffer(flags); - - PyBuffer.Pointer bp = view.getBuf(); - assertBytesEqual("buffer does not match reference", test.material.bytes, bp); - - } else { - // The client will have to navigate with the strides array - int flags = test.readonly ? PyBUF.STRIDED_RO : PyBUF.STRIDED; - PyBuffer view = test.subject.getBuffer(flags); - - stride = view.getStrides()[0]; // Just possibly != test.strides when length<=1 - PyBuffer.Pointer bp = view.getBuf(); - assertBytesEqual("buffer does not match reference", test.material.bytes, bp, stride); - } - + /** Test method for {@link org.python.core.PyBuffer#hasArray()}. */ + @Test + public void testHasArray() { + announce("hasArray"); + if (spec.hasArray) { + assertTrue("a backing array was expected", view.hasArray()); + } else { + assertFalse("no backing array was expected", view.hasArray()); } } - /** - * Test method for {@link org.python.core.PyBuffer#getPointer(int)}. - */ + /** Test method for {@link org.python.core.PyBuffer#getBuf()}. */ + @Test + @SuppressWarnings("deprecation") + public void testGetBuf() { + announce("getBuf"); + if (spec.hasArray) { + int stride = spec.getStride(); + PyBuffer.Pointer bp = view.getBuf(); + assertBytesEqual("buffer does not match reference", ref.bytes, bp, stride); + } + } + + /** Test method for {@link org.python.core.PyBuffer#getPointer(int)}. */ + @Test + @SuppressWarnings("deprecation") public void testGetPointer() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getPointer: " + test); - } - PyBuffer view = test.view; - int n = test.material.length, itemsize = view.getItemsize(); - byte[] exp = new byte[itemsize], bytes = test.material.bytes; + announce("getPointer"); + if (spec.hasArray) { + int itemsize = spec.getItemsize(); + byte[] exp = new byte[itemsize], bytes = ref.bytes; - for (int i = 0; i < n; i++) { + // Try to get a pointer to an item at each byte location in the buffer + for (int i = 0; i <= ref.length - itemsize; i++) { // Expected result is one item (allow for itemsize) int p = i * itemsize; for (int j = 0; j < itemsize; j++) { @@ -1018,18 +1115,15 @@ } } - /** - * Test method for {@link org.python.core.PyBuffer#getPointer(int[])}. - */ + /** Test method for {@link org.python.core.PyBuffer#getPointer(int[])}. */ + @Test + @SuppressWarnings("deprecation") public void testGetPointerNdim() { int[] index = new int[1]; - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getPointer(array): " + test); - } - PyBuffer view = test.view; - int n = test.material.length, itemsize = view.getItemsize(); - byte[] exp = new byte[itemsize], bytes = test.material.bytes; + announce("getPointer(array)"); + if (spec.hasArray) { + int n = ref.length, itemsize = view.getItemsize(); + byte[] exp = new byte[itemsize], bytes = ref.bytes; for (int i = 0; i < n; i++) { // Expected result is one item (allow for itemsize) @@ -1055,118 +1149,127 @@ } } - /** - * Test method for {@link org.python.core.PyBUF#getStrides()}. - */ + /** Test method for {@link org.python.core.PyBUF#getStrides()}. */ + @Test public void testGetStrides() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getStrides: " + test); - } - for (int flags : test.validFlags) { - PyBuffer view = test.subject.getBuffer(flags); - // Strides array irrespective of the client flags ... (different from CPython) - int[] strides = view.getStrides(); - assertNotNull("strides[] should always be provided", strides); - - // The strides must have the expected value if length >1 - if (test.material.bytes.length > 1) { - assertIntsEqual("unexpected strides", test.strides, strides); - } + announce("getStrides"); + for (int flags : spec.validFlags) { + PyBuffer view = this.view.getBuffer(flags); + // Strides array irrespective of the client flags ... (different from CPython) + int[] strides = view.getStrides(); + assertNotNull("strides[] should always be provided", strides); + // The strides must have the expected value if length >1 + if (ref.bytes.length > 1) { + assertIntsEqual("unexpected strides", spec.strides, strides); } } } - /** - * Test method for {@link org.python.core.PyBUF#getSuboffsets()}. - */ + /** Test method for {@link org.python.core.PyBUF#getSuboffsets()}. */ + @Test public void testGetSuboffsets() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getSuboffsets: " + test); - } - // Null for all test material - assertNull(test.view.getSuboffsets()); - } + announce("getSuboffsets"); + // Null for all test material + assertNull(view.getSuboffsets()); + } - /** - * Test method for {@link org.python.core.PyBUF#isContiguous(char)}. - */ + /** Test method for {@link org.python.core.PyBUF#isContiguous(char)}. */ + @Test public void testIsContiguous() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("isContiguous: " + test); - } - // True for all test material and orders (since 1-dimensional) - for (String orderMsg : validOrders) { - char order = orderMsg.charAt(0); - assertTrue(orderMsg, test.view.isContiguous(order)); - } + announce("isContiguous"); + // All test material is 1-dimensional so it's fairly simple and same for all orders + int ndim = spec.shape[0], stride = spec.getStride(), itemsize = spec.getItemsize(); + boolean contig = ndim < 2 || stride == itemsize; + for (String orderMsg : validOrders) { + char order = orderMsg.charAt(0); + assertEquals(orderMsg, view.isContiguous(order), contig); } } private static final String[] validOrders = {"C-contiguous test fail", "F-contiguous test fail", "Any-contiguous test fail"}; - /** - * Test method for {@link org.python.core.PyBuffer#getFormat()}. - */ + /** Test method for {@link org.python.core.PyBuffer#getFormat()}. */ + @Test public void testGetFormat() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getFormat: " + test); - } - for (int flags : test.validFlags) { - PyBuffer view = test.subject.getBuffer(flags); - // Format given irrespective of the client flags ... (different from CPython) - assertNotNull("format should always be provided", view.getFormat()); - assertEquals("B", view.getFormat()); - // And, we can ask for it explicitly ... - view = test.subject.getBuffer(flags | PyBUF.FORMAT); - assertEquals("B", view.getFormat()); - } + announce("getFormat"); + TestSpec spec = this.spec; + + for (int flags : spec.validFlags) { + PyBuffer view = this.view.getBuffer(flags); + // Format given irrespective of the client flags ... (different from CPython) + assertNotNull("format should always be provided", view.getFormat()); + assertEquals("B", view.getFormat()); + // And, we can ask for it explicitly ... + view = this.view.getBuffer(flags | PyBUF.FORMAT); + assertEquals("B", view.getFormat()); } } - /** - * Test method for {@link org.python.core.PyBUF#getItemsize()}. - */ + /** Test method for {@link org.python.core.PyBUF#getItemsize()}. */ + @Test public void testGetItemsize() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("getItemsize: " + test); - } - // Unity for all test material - assertEquals(1, test.view.getItemsize()); - } + announce("getItemsize"); + // Unity for all test material + assertEquals(1, view.getItemsize()); + } + + /** Test method for {@link org.python.core.PyBuffer#toString()}. */ + @Test + public void testToString() { + announce("toString"); + String r = view.toString(); + assertEquals("buffer does not match reference", ref.string, r); } /** - * Test method for {@link org.python.core.PyBuffer#toString()}. + * Custom assert method comparing the bytes at a {@link PyBuffer.Pointer} to those in a byte + * array, when that Pointer is obtained from a contiguous PyBuffer. + * Let bp[i] denote bp.storage[bp.offset+i], by analogy with a C + * pointer. It is required that bp[k] == expected[k], for every index in + * expected. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param bp result to test */ - public void testToString() { - for (BufferTestPair test : buffersToRead) { - if (verbosity > 0) { - System.out.println("toString: " + test); - } - String r = test.view.toString(); - assertEquals("buffer does not match reference", test.material.string, r); - } + @SuppressWarnings("deprecation") + private static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp) { + assertBytesEqual(message, expected, bp, 1); + } + + /** + * Custom assert method comparing the bytes at a {@link PyBuffer.Pointer} to those in a byte + * array, when that Pointer is obtained from a striding PyBuffer. Let + * bp[i] denote bp.storage[bp.offset+i], by analogy with a C pointer. + * It is required that bp[k*stride] == expected[k], for every index k + * in expected. If not, a fail() is declared. + * + * @param message to issue on failure + * @param expected expected byte array + * @param bp result to test + * @param stride in the bp.storage array + */ + @SuppressWarnings("deprecation") + private static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp, + int stride) { + ByteBufferTestSupport.assertBytesEqual(message, expected, 0, expected.length, bp.storage, + bp.offset, stride); } /* - * ------------------------------------------------------------------------------------------- A - * series of custom exporters to permit testing abstracted from the Jython interpreter. These + * -------------------------------------------------------------------------------------------- + * A series of custom exporters to permit testing abstracted from the Jython interpreter. These * use the implementation classes in org.python.core.buffer in ways very similar to the * implementations of bytearray and str. - * ------------------------------------------------------------------------------------------- + * -------------------------------------------------------------------------------------------- */ /** - * A class to act as an exporter that uses the SimpleReadonlyBuffer. The exporter exports a new - * PyBuffer object to each consumer (although each references the same internal storage) and it - * does not track their fate. You are most likely to use this approach with an exporting object - * that is immutable (or at least fixed in size). + * A class to act as an exporter that uses the SimpleBuffer. The exporter exports a new PyBuffer + * object to each consumer (although each references the same internal storage) and it does not + * track their fate. You are most likely to use this approach with an exporting object that is + * immutable (or at least fixed in size). */ static class SimpleExporter implements BufferProtocol { @@ -1183,7 +1286,17 @@ @Override public PyBuffer getBuffer(int flags) { - return new SimpleBuffer(flags, storage); + return new SimpleBuffer(flags, this, storage); + } + + } + + /** A factory for SimpleBuffer objects used in genTestSpects and some tests. */ + private static class SimpleExporterFactory extends ReadonlyExporterFactory { + + @Override + public BufferProtocol make(ByteMaterial m) { + return new SimpleExporter(m.getBytes()); } } @@ -1196,7 +1309,8 @@ protected Reference export; /** - * Try to re-use existing exported buffer, or return null if can't. + * Try to re-use existing exported buffer, or return null if can't: modelled after the + * buffer re-use strategy in {@link PyByteArray}. */ protected BaseBuffer getExistingBuffer(int flags) { BaseBuffer pybuf = null; @@ -1261,7 +1375,7 @@ BaseBuffer pybuf = getExistingBuffer(flags); if (pybuf == null) { // No existing export we can re-use - pybuf = new SimpleStringBuffer(flags, storage); + pybuf = new SimpleStringBuffer(flags, this, storage); // Hold a reference for possible re-use export = new SoftReference(pybuf); } @@ -1278,7 +1392,7 @@ * avoiding the cost of duplicate buffers. This is the case with PyByteArray, which prohibits * operations that would resize it, while there are outstanding exports. */ - static class SimpleWritableExporter extends TestableExporter { + private static class SimpleWritableExporter extends TestableExporter { protected byte[] storage; @@ -1297,7 +1411,7 @@ BaseBuffer pybuf = getExistingBuffer(flags); if (pybuf == null) { // No existing export we can re-use - pybuf = new SimpleWritableBuffer(flags, storage) { + pybuf = new SimpleWritableBuffer(flags, this, storage) { @Override protected void releaseAction() { @@ -1313,471 +1427,120 @@ } + /** A class to act as an exporter that uses the RollYourOwnArrayBuffer class. */ + private static class RollYourOwnExporter extends TestableExporter { + + protected byte[] storage; + + public RollYourOwnExporter(byte[] storage) { + this.storage = storage; + } + + @Override + public PyBuffer getBuffer(int flags) { + // If we have already exported a buffer it may still be available for re-use + BaseBuffer pybuf = getExistingBuffer(flags); + if (pybuf == null) { + // No existing export we can re-use + pybuf = new RollYourOwnArrayBuffer(flags, this, storage); + // Hold a reference for possible re-use + export = new WeakReference(pybuf); + } + return pybuf; + } + + } + /** - * Class to hold test material representing the same sequence of values 0..255 in several - * different ways. + * Minimal extension of BaseBuffer in order to test the default implementations there. They're + * slow, so mostly we override them in the implementations BaseArrayBuffer and BaseNIOBuffer, + * but they still have to be correct. The class represents a one-dimensional, strided array of + * bytes, so it can represent a slice of itself. */ - protected static class ByteMaterial { + private static class RollYourOwnArrayBuffer extends BaseBuffer { - final String string; - final byte[] bytes; - final int[] ints; - final int length; + final static int FEATURES = PyBUF.WRITABLE | PyBUF.AS_ARRAY; - /** Construct from String. */ - public ByteMaterial(String s) { - string = s; - length = s.length(); - bytes = new byte[length]; - ints = new int[length]; - for (int i = 0; i < length; i++) { - int x = s.charAt(i); - ints[i] = x; - bytes[i] = (byte)x; + final byte[] storage; + final PyBuffer root; + + /** + * Create a buffer view of the entire array. + * + * @param flags consumer requirements + * @param obj exporting object (or null) + * @param storage byte array exported in its entirety + */ + public RollYourOwnArrayBuffer(int flags, BufferProtocol obj, byte[] storage) { + this(flags, null /* =this */, obj, storage, 0, storage.length, 1); + } + + /** + * Construct a slice of a one-dimensional byte array. + * + * @param flags consumer requirements + * @param root on which release must be called when this is released + * @param obj exporting object (or null) + * @param storage raw byte array containing exported data + * @param index0 index into storage of item[0] + * @param count number of items in the slice + * @param stride in between successive elements of the new PyBuffer + * @throws PyException (BufferError) when expectations do not correspond with the type + */ + public RollYourOwnArrayBuffer(int flags, PyBuffer root, BufferProtocol obj, byte[] storage, + int index0, int count, int stride) throws IndexOutOfBoundsException, + NullPointerException, PyException { + // Client will need to navigate using shape and strides if this is a slice + super(FEATURES | ((index0 == 0 && stride == 1) ? 0 : STRIDES), // + index0, new int[] {count}, new int[] {stride}); + this.storage = storage; + // Check the potential index range + if (count > 0) { + int end = index0 + (count - 1) * stride; + final int END = storage.length - 1; + if (index0 < 0 || index0 > END || end < 0 || end > END) { + throw new IndexOutOfBoundsException(); + } + } + // Check client is compatible + checkRequestFlags(flags); + // Get a lease on the root PyBuffer (read-only). Last in case a check above fails. + if (root == null) { + this.root = this; + this.obj = obj; + } else { + this.root = root.getBuffer(FULL_RO); + this.obj = root.getObj(); } } - /** Construct from byte array. */ - public ByteMaterial(byte[] b) { - length = b.length; - StringBuilder buf = new StringBuilder(length); - bytes = new byte[length]; - ints = new int[length]; - for (int i = 0; i < length; i++) { - int x = 0xff & b[i]; - ints[i] = x; - bytes[i] = (byte)x; - buf.appendCodePoint(x); - } - string = buf.toString(); - } - - /** Construct from int array. */ - public ByteMaterial(int[] a) { - length = a.length; - StringBuilder buf = new StringBuilder(length); - bytes = new byte[length]; - ints = new int[length]; - for (int i = 0; i < length; i++) { - int x = a[i]; - ints[i] = x; - bytes[i] = (byte)x; - buf.appendCodePoint(x); - } - string = buf.toString(); - } - - /** Construct from pattern on values (used modulo 256). */ - public ByteMaterial(int start, int count, int inc) { - length = count; - StringBuilder buf = new StringBuilder(length); - bytes = new byte[length]; - ints = new int[length]; - int x = start; - for (int i = 0; i < length; i++) { - ints[i] = x; - bytes[i] = (byte)x; - buf.appendCodePoint(x); - x = (x + inc) & 0xff; - } - string = buf.toString(); + @Override + protected PyBuffer getRoot() { + return root; } @Override - public String toString() { - StringBuilder buf = new StringBuilder(100); - buf.append("byte[").append(length).append("]={ "); - for (int i = 0; i < length; i++) { - if (i > 0) { - buf.append(", "); - } - if (i >= 5) { - buf.append(" ..."); - break; - } else { - buf.append(ints[i]); - } - } - buf.append(" }"); - return buf.toString(); - } - - /** - * @return a copy of the bytes array (that the client is allowed to modify) - */ - byte[] getBytes() { - return bytes.clone(); - } - - /** - * Create material equivalent to a slice. this will not be used to create an exporter, but - * rather to specify data equivalent to the export. - * - * @param start first index to include - * @param length number of indices - * @param stride between indices - * @return ByteMaterial in which the arrays are a slice of this one - */ - ByteMaterial slice(int start, int length, int stride) { - return new ByteMaterial(sliceBytes(bytes, start, length, stride)); - } - } - - /** - * Create a byte array from the values of the PyBuffer obtained using - * {@link PyBuffer#byteAt(int)}, to a length obtained from {@link PyBuffer#getLen()}. - * - * @param v the buffer - * @return the byte array - */ - static byte[] bytesFromByteAt(PyBuffer v) { - final int N = v.getLen(); - byte[] a = new byte[N]; - for (int i = 0; i < N; i++) { - a[i] = v.byteAt(i); - } - return a; - } - - /** - * Create a byte array that is a strided copy of the one passed in. The specifications are - * assumed correct for the size of that array. - * - * @param b source array - * @param start first index to include - * @param length number of indices - * @param stride between indices - * @return slice of b - */ - static byte[] sliceBytes(byte[] b, int start, int length, int stride) { - byte[] a = new byte[length]; - for (int i = 0, j = start; i < length; i++, j += stride) { - a[i] = b[j]; - } - return a; - } - - /** - * Customised assert method comparing a buffer pointer to a byte array, usually the one from - * ByteMaterial. - * - * @param message to issue on failure - * @param expected expected byte array - * @param bp result to test - */ - static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp) { - assertBytesEqual(message, expected, bp, 1); - } - - /** - * Customised assert method comparing a buffer pointer to a byte array, usually the one from - * ByteMaterial. - * - * @param message to issue on failure - * @param expected expected byte array - * @param bp result to test - * @param stride in the storage array - */ - static void assertBytesEqual(String message, byte[] expected, PyBuffer.Pointer bp, int stride) { - assertBytesEqual(message, expected, 0, expected.length, bp.storage, bp.offset, stride); - } - - /** - * Customised assert method comparing a buffer pointer to a byte array, usually the one from - * ByteMaterial. - * - * @param message to issue on failure - * @param expected expected byte array - * @param expectedStart where to start the comparison in expected - * @param n number of bytes to test - * @param bb result to test - * @param stride in the storage array - */ - static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, - PyBuffer.Pointer bp, int stride) { - assertBytesEqual(message, expected, expectedStart, n, bp.storage, bp.offset, stride); - } - - /** - * Customised assert method comparing a byte arrays: values in the actual value must match all - * those in expected[], and they must be the same length. - * - * @param message to issue on failure - * @param expected expected byte array - * @param actual result to test - */ - static void assertBytesEqual(String message, byte[] expected, byte[] actual) { - assertEquals(message + " (array size)", expected.length, actual.length); - assertBytesEqual(message, expected, 0, expected.length, actual, 0, 1); - } - - /** - * Customised assert method comparing byte arrays: values in the actual value starting at - * actual[actualStart] must match all those in expected[], and there must be enough of them. - * - * @param message to issue on failure - * @param expected expected byte array - * @param actual result to test - * @param actualStart where to start the comparison in actual - */ - static void assertBytesEqual(String message, byte[] expected, byte[] actual, int actualStart) { - assertBytesEqual(message, expected, 0, expected.length, actual, actualStart, 1); - } - - /** - * Customised assert method comparing byte arrays: values starting at actual[actualStart] must - * those starting at expected[expectedStart], for a distance of n bytes. - * - * @param message to issue on failure - * @param expected expected byte array - * @param expectedStart where to start the comparison in expected - * @param n number of bytes to test - * @param actual result to test - * @param actualStart where to start the comparison in actual - */ - static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, - byte[] actual, int actualStart) { - assertBytesEqual(message, expected, expectedStart, n, actual, actualStart, 1); - } - - /** - * Customised assert method comparing byte arrays: values starting at actual[actualStart] must - * those starting at expected[expectedStart], for a distance of n bytes. - * - * @param message to issue on failure - * @param expected expected byte array - * @param expectedStart where to start the comparison in expected - * @param n number of bytes to test - * @param actual result to test - * @param actualStart where to start the comparison in actual - * @param stride spacing of bytes in actual array - */ - static void assertBytesEqual(String message, byte[] expected, int expectedStart, int n, - byte[] actual, int actualStart, int stride) { - - if (actualStart < 0) { - fail(message + " (start<0 in result)"); - - } else if (expectedStart < 0) { - fail(message + " (start<0 in expected result): bug in test?"); - - } else if (actualStart + (n - 1) * stride + 1 > actual.length) { - fail(message + " (result too short)"); - - } else if (expectedStart + n > expected.length) { - fail(message + " (expected result too short): bug in test?"); - - } else { - // Should be safe to compare the values - int i = actualStart, j, jLimit = expectedStart + n; - for (j = expectedStart; j < jLimit; j++) { - if (actual[i] != expected[j]) { - break; - } - i += stride; - } - - // If we stopped early, diagnose the problem - if (j < jLimit) { - System.out.println(" expected:" - + Arrays.toString(Arrays.copyOfRange(expected, expectedStart, expectedStart - + n))); - System.out - .println(" actual:" - + Arrays.toString(Arrays.copyOfRange(actual, actualStart, - actualStart + n))); - System.out.println(" _actual_:" + Arrays.toString(actual)); - fail(message + " (byte at " + j + ")"); - } - } - } - - /** - * Customised assert method comparing a int arrays: values in the actual value starting at - * actual[offset] must match all those in expected[], and there must be enough of them. - * - * @param message to issue on failure - * @param expected expected array - * @param actual result to test - * @param offset where to start the comparison in actual - */ - static void assertIntsEqual(String message, int[] expected, int[] actual, int offset) { - int n = expected.length; - if (offset < 0) { - fail(message + " (offset<0)"); - } else if (offset + n > actual.length) { - fail(message + " (too short)"); - } else { - // Should be safe to compare the values - int i = offset, j; - for (j = 0; j < n; j++) { - if (actual[i++] != expected[j]) { - break; - } - } - if (j < n) { - System.out.println(" expected:" + Arrays.toString(expected)); - System.out.println(" actual:" + Arrays.toString(actual)); - fail(message + " (int at " + j + ")"); - } - } - } - - /** - * Customised assert method comparing a int arrays: int in the actual value must match all those - * in expected[], and there must be the same number of them. - * - * @param message to issue on failure - * @param expected expected array - * @param actual result to test - */ - static void assertIntsEqual(String message, int[] expected, int[] actual) { - int n = expected.length; - assertEquals(message, n, actual.length); - // Should be safe to compare the values - int j; - for (j = 0; j < n; j++) { - if (actual[j] != expected[j]) { - break; - } - } - if (j < n) { - System.out.println(" expected:" + Arrays.toString(expected)); - System.out.println(" actual:" + Arrays.toString(actual)); - fail(message + " (int at " + j + ")"); - } - } - - /** - * Within a given test case (e.g. the test of one particular method) we run many data sets, and - * these are created by {@link PyBufferTest#setUp()} as instances of this class. The main - * contents of the BufferTestPair are the test subject and the material. The subject may be one - * of the base objects specified in 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. - */ - 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}; - - /** The shape array that the subject should match (will be single element in present tests) */ - int[] shape; - - /** The shape array that the subject should match (will be single element in present tests) */ - int[] strides; - - /** - * A subject and its reference material, together with explicit shape and strides arrays - * expected. - * - * @param subject of the test - * @param material containing a Java byte array that a view of the subject should equal - * @param shape of the array, when testing in N-dimensions - * @param strides of the array, when testing sliced views - * @param readonly if true the base exporter can only provide read-only views - */ - public BufferTestPair(BufferProtocol subject, ByteMaterial material, int[] shape, - int[] strides, boolean readonly, int[] validFlags, int[] validTassles) { - this.subject = subject; - this.material = new ByteMaterial(material.ints); // Copy in case modified - this.shape = shape; - this.strides = strides; - this.readonly = readonly; - this.validFlags = validFlags; - this.validTassles = validTassles; - - int flags = readonly ? PyBUF.FULL_RO : PyBUF.FULL; - - if (subject instanceof PyBuffer) { - this.view = (PyBuffer)subject; - } else { - PyBuffer v = null; - try { - // System.out.printf("BufferTestPair: length=%d, readonly=%s\n", - // material.length, readonly); - v = subject.getBuffer(flags); - } catch (Exception e) { - /* - * We ignore this case if we fail, because we are not testing buffer creation - * here, but making buffers to be tested. We'll test buffer creation in - * testGetBuffer. - */ - } - this.view = v; - } - } - - /** - * Short constructor for contiguous arrays in one dimension. - * - * @param subject of the test - * @param material containing a Java byte array that a view of the subject should equal - * @param readonly if true the base exporter can only provide read-only views - */ - public BufferTestPair(BufferProtocol subject, ByteMaterial material, boolean readonly) { - this(subject, material, new int[1], STRIDES_1D, readonly, simpleFlags, simpleTassles); - shape[0] = material.length; - } - - /** - * Short constructor for strided arrays in one dimension. - * - * @param subject of the test - * @param material containing a Java byte array that a view of the subject should equal - * @param stride of the array, when testing sliced views - * @param readonly if true the base exporter can only provide read-only views - */ - public BufferTestPair(PyBuffer subject, ByteMaterial material, int stride, boolean readonly) { - this(subject, material, new int[1], new int[1], readonly, strided1DFlags, - strided1DTassles); - shape[0] = material.length; - strides[0] = stride; + public PyBuffer getBufferSlice(int flags, int start, int length, int stride) { + int newStart = index0 + start * strides[0]; + int newStride = strides[0] * stride; + return new RollYourOwnArrayBuffer(flags, root, null, storage, newStart, length, + newStride); } @Override - public String toString() { - int offset = view.getBuf().offset; - String offsetSpec = offset > 0 ? "[0@(" + offset + "):" : "[:"; - int stride = strides[0]; - String sliceSpec = offsetSpec + shape[0] + (stride != 1 ? "*(" + stride + ")]" : "]"); - return subject.getClass().getSimpleName() + sliceSpec + " ( " + material.toString() - + " )"; + public ByteBuffer getNIOByteBufferImpl() { + return ByteBuffer.wrap(storage); } + @Override + protected byte byteAtImpl(int byteIndex) { + return storage[byteIndex]; + } + + @Override + protected void storeAtImpl(byte value, int byteIndex) throws IndexOutOfBoundsException, + PyException { + storage[byteIndex] = value; + } } } diff --git a/tests/java/org/python/core/PyBufferTestSupport.java b/tests/java/org/python/core/PyBufferTestSupport.java new file mode 100644 --- /dev/null +++ b/tests/java/org/python/core/PyBufferTestSupport.java @@ -0,0 +1,544 @@ +package org.python.core; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import org.python.core.ByteBufferTestSupport.ByteMaterial; + +/** + * Supporting test fixtures for testing {@link PyBuffer} implementations, this class provides means + * to generate test specifications and organise them into a list. This object creates and holds + * factories for the multiple examples of the several implementation types the PyBufferTest needs, + * together with the configuration the factory and the test need. + */ +public class PyBufferTestSupport { + + /** Control amount of output while generating material. */ + protected int verbosity; + + /** Lengths we will use if we can when slicing view */ + private final int[] sliceLengths; + + /** Step sizes we will use if we can when slicing view */ + private final int[] sliceSteps; + + /** List of test data configurations. */ + private List testSpecList = new LinkedList(); + + /** + * Create an instance, and choose the number an variety of tests that each call to + * {@link #generate(BufferProtocol, ByteMaterial, boolean)} will produce. + * + * @param sliceLengths what length of slices to try to make from each original + * @param sliceSteps step sizes (strides) to try to use + */ + PyBufferTestSupport(int[] sliceLengths, int[] sliceSteps) { + this(0, sliceLengths, sliceSteps); + } + + /** + * Create an instance, and choose the number an variety of tests that each call to + * {@link #generate(BufferProtocol, ByteMaterial, boolean)} will produce. + * + * @param verbosity how much noise to make when generating test data + * @param sliceLengths what length of slices to try to make from each original + * @param sliceSteps step sizes (strides) to try to use + */ + PyBufferTestSupport(int verbosity, int[] sliceLengths, int[] sliceSteps) { + this.verbosity = verbosity; + this.sliceLengths = sliceLengths; + this.sliceSteps = sliceSteps; + } + + /** + * Add to the test queue a series of test specifications for a particular type of exporter and + * byte material, in various sliced versions. The first argument provides a factory able to + * construct a test object bearing the {@link BufferProtocol} interface, from the + * {@link ByteMaterial} also supplied. The first test specification queued is based directly on + * such construction. Construction takes place when {@link TestSpec#make()} is called during the + * test constructor. + *

+ * The method goes on to create a series of specifications that when invoked in test + * initialisation will provide sliced views. + *

+ * When the test runs, it will be given one test specification. Either: + *

    + *
  1. the test is given the original root specification and makes a PyBuffer from + * it, by a call to {@link TestSpec#make()}, whose implementation creates a test subject of + * appropriate type, or
  2. + *
  3. the test is given a derived sliced specification and makes a buffer from it, by a call to + * {@link TestSpec#make()}, whose implementation slices a buffer provided by the original root + * specification.
  4. + *
+ * The slices are made with a variety of argument combinations, filtered down to those that make + * sense for the size of the direct view. The reference value in the derived specification + * {@link TestSpec#ref} is computed independently of the test subject, from the slice + * specification and a the reference value in the root specification. + * + * @param original to specify a test and from which to generate other tests + */ + void add(ExporterFactory factory, ByteMaterial material) { + + // Add test using the specification passed as arguments + TestSpec original = new TestSpec(factory, material); + queue(original); + + // Generate some slices from the material and this direct view + int N = original.ref.length; + int M = (N + 4) / 4; // At least one and about N/4 + + // For a range of start positions up to one beyond the end + for (int start = 0; start <= N; start += M) { + // For a range of lengths + for (int length : sliceLengths) { + + if (length == 0) { + queue(original, start, 0, 1); + queue(original, start, 0, 2); + + } else if (length == 1 && start < N) { + queue(original, start, 1, 1); + queue(original, start, 1, 2); + + } else if (start < N) { + + // And for a range of step sizes + for (int step : sliceSteps) { + // Check this is a feasible slice + if (start + (length - 1) * step < N) { + queue(original, start, length, step); + } + } + + // Now use all the step sizes negatively + for (int step : sliceSteps) { + // Check this is a feasible slice + if (start - (length - 1) * step >= 0) { + queue(original, start, length, -step); + } + } + } + } + } + } + + /** Generate and queue one test of non-slice type (if getting a buffer succeeds). */ + private void queue(TestSpec spec) { + if (verbosity > 2) { + System.out.printf("queue non-slice: length=%d, readonly=%s\n", spec.ref.length, + spec.readonly); + } + testSpecList.add(spec); + } + + /** Generate and queue one test of slice type (if getting a buffer succeeds). */ + private void queue(TestSpec original, int start, int length, int step) { + /* + * Make a slice. We ignore this case if we fail, because we are not testing slice creation + * here, but making slices to be tested as buffers. We'll test slice creation in + * testGetBufferSlice. + */ + try { + if (verbosity > 2) { + System.out.printf(" queue slice: start=%4d, length=%4d, step=%4d\n", start, + length, step); + } + TestSpec spec = new SlicedTestSpec(original, 1, start, length, step); + testSpecList.add(spec); + } catch (Exception e) { + /* + * We ignore this case if we fail, because we are not testing slice creation here, but + * making slices to be tested as buffers. We'll test slice creation elsewhere. + */ + if (verbosity > 2) { + System.out.printf("*** SKIP %s\n", e); + } + } + } + + /** + * Return a copy of the generated list of test data in a form suitable for test construction + * with a JUnit parameterised runner, which is as a collection of arrays of objects, where each + * array becomes the arguments to the test constructor. (@see org.junit.runners.Parameterized) + * + * @return generated list of test data + */ + List getTestData() { + List r = new ArrayList(testSpecList.size()); + for (TestSpec spec : testSpecList) { + r.add(new TestSpec[] {spec}); + } + return r; + } + + /** + * Create a byte array from the values of the PyBuffer obtained using + * {@link PyBuffer#byteAt(int)}, to a length obtained from {@link PyBuffer#getLen()}. + * + * @param v the buffer + * @return the byte array + */ + static byte[] bytesFromByteAt(PyBuffer v) { + final int N = v.getLen(); + byte[] a = new byte[N]; + for (int i = 0; i < N; i++) { + a[i] = v.byteAt(i); + } + return a; + } + + /** + * Interface to a factory capable of making a {@link PyBuffer} exporter from + * {@link ByteMaterial}. + */ + interface ExporterFactory { + + /** Make fresh test object. */ + BufferProtocol make(ByteMaterial m); + + /** Whether the test object will be read-only. */ + boolean isReadonly(); + + /** Whether the test object will be able to provide access as a byte array. */ + boolean hasArray(); + } + + abstract static class ReadonlyExporterFactory implements ExporterFactory { + + @Override + public boolean isReadonly() { + return true; + } + + @Override + public boolean hasArray() { + return true; + } + + }; + + abstract static class WritableExporterFactory implements ExporterFactory { + + @Override + public boolean isReadonly() { + return false; + } + + @Override + public boolean hasArray() { + return true; + } + + }; + + /** + * Class holding reference data for a test and a factory method that will produce an object with + * interface {@link BufferProtocol} for use in tests. The class has one principal method + * {@link TestSpec#makePair()}, which must return an {@link ObjectAndView} where the view + * element is equal to the reference {@link TestSpec#ref}. During a JUnit test, the test + * constructor will be called with a particular instance of this class and will call + * makePair() one or more times to get fresh test material. + */ + static class TestSpec { + + /** Factory for test objects. */ + final ExporterFactory factory; + /** The value of the associated test object. */ + final ByteMaterial ref; + /** The associated PyBuffer should be read-only. */ + final boolean readonly; + /** The associated PyBuffer should be accessible as a JVM array. */ + final boolean hasArray; + /** Parent TestSpec, when this is a derived one, or null if it is an original. */ + final TestSpec parent; + /** The value of shape array that the view should have that matches {@link #ref}. */ + final int[] shape; + /** The value of strides array that the view should have that matches {@link #ref}. */ + final int[] strides; + + /** Either {@link PyBUF#FULL_RO} or {@link PyBUF#FULL} according to {@link #readonly}. */ + final int flags; + + /** Allowable basic flag combinations, such as {@link PyBUF#STRIDES}. */ + final int[] validFlags; + + /** Allowable additional flag combinations, such as {@link PyBUF#FORMAT} */ + final int[] validTassles; + + /** + * A one-dimensional exporter should be able to give us a buffer for all these flag types. + */ + static final int[] simpleFlags = {PyBUF.SIMPLE, PyBUF.ND, PyBUF.STRIDES, PyBUF.INDIRECT, + PyBUF.FULL_RO}; + + /** To {@link #simpleFlags} we can add any of these */ + static final int[] simpleTassles = {0, PyBUF.FORMAT, PyBUF.C_CONTIGUOUS, + PyBUF.F_CONTIGUOUS, PyBUF.ANY_CONTIGUOUS}; + + /** + * Construct a specification for a 1D contiguous byte-array based on the exporter factory + * and reference data supplied. + * + * @param factory makes exporters of the particular type + * @param ref the fill those exporters should have + */ + TestSpec(ExporterFactory factory, ByteMaterial ref) { + this(null, factory, ref, new int[] {ref.length}, new int[] {1}, simpleFlags, + simpleTassles); + } + + /** + * Construct a specification for a 1D contiguous item-array based on the exporter factory, + * shape data and reference data supplied. + * + * @param parent of this test specification + * @param ref the fill those exporters should have (also determines the item size) + * @param shape array defining number and size of dimensions (as {@link PyBUF#getShape()} + * @param strides array defining addressing polynomial (as {@link PyBUF#getStrides()}) + * @param validFlags allowable basic flag combinations usable with this specification + * @param validTassles allowable additional flag combinations + */ + protected TestSpec(TestSpec parent, ByteMaterial ref, int[] shape, int[] strides, + int[] validFlags, int[] validTassles) { + this(parent, parent.getOriginal().factory, ref, shape, strides, validFlags, + validTassles); + } + + /** + * Construct a specification for a 1D contiguous item-array based on the exporter factory, + * shape data and reference data supplied. + * + * @param parent of this test specification + * @param factory makes exporters of the particular type, given ref + * @param ref the fill those exporters should have (also determines the item size) + * @param shape array defining number and size of dimensions (as {@link PyBUF#getShape()} + * @param strides array defining addressing polynomial (as {@link PyBUF#getStrides()}) + * @param validFlags allowable basic flag combinations usable with this specification + * @param validTassles allowable additional flag combinations + */ + protected TestSpec(TestSpec parent, ExporterFactory factory, ByteMaterial ref, int[] shape, + int[] strides, int[] validFlags, int[] validTassles) { + this.parent = parent; + this.factory = factory; + this.readonly = factory.isReadonly(); + this.hasArray = factory.hasArray(); + this.flags = (readonly ? PyBUF.FULL_RO : PyBUF.FULL) | (hasArray ? PyBUF.AS_ARRAY : 0); + this.ref = ref; + this.shape = shape; + this.strides = strides; + this.validFlags = validFlags; + this.validTassles = validTassles; + } + + /** Return the parent of this specification (or null when it is an original). */ + final TestSpec getParent() { + return parent; + } + + /** This is an original specification (parent is null). */ + final boolean isOriginal() { + return parent == null; + } + + /** Return the original of this specification (ancestor with no parent). */ + final TestSpec getOriginal() { + TestSpec p = this; + while (!p.isOriginal()) { + p = p.getParent(); + } + return p; + } + + /** Return the item size. */ + int getItemsize() { + return 1; + } + + /** Return the stride that a buffer made from this specification should have. */ + int getStride() { + return strides[0]; + } + + /** Return the start index that a buffer made from this specification should have. */ + int getStart() { + return 0; + } + + /** Simple holder class for a buffer exporter object and a related buffer. */ + static class ObjectAndView { + + final BufferProtocol obj; + final PyBuffer view; + + ObjectAndView(BufferProtocol obj, PyBuffer view) { + this.obj = obj; + this.view = view; + } + } + + /** + * Make the test object which must implement BufferProtocol and its + * PyBuffer view. The value as a byte array must equal {@link #ref}. + */ + public ObjectAndView makePair() { + BufferProtocol obj = factory.make(ref); + PyBuffer view = obj.getBuffer(flags); + return new ObjectAndView(obj, view); + } + + @SuppressWarnings("deprecation") + @Override + public String toString() { + + ObjectAndView pair = makePair(); + BufferProtocol obj = pair.obj; + PyBuffer view = pair.view; + + StringBuilder sb = new StringBuilder(100); + sb.append(obj.getClass().getSimpleName()).append('['); + + int offset, stride = getStride(); + + if (view.hasArray()) { + offset = view.getBuf().offset; + } else { + offset = view.getNIOByteBuffer().position(); + } + + if (offset > 0) { + sb.append(offset); + } + + String plus = offset == 0 ? "" : "+"; + + if (stride == 1) { + sb.append(plus).append("k]"); + } else if (stride == -1) { + sb.append("-k]"); + } else if (stride < 0) { + sb.append("-").append(-stride).append("*k]"); + } else { + /* stride>1 or ==0) */sb.append(plus).append(stride).append("*k]"); + } + + while (sb.length() < 30) { + sb.append(' '); + } + sb.append(view.isReadonly()?"R ":"W "); + sb.append("ref = ").append(ref.toString()); + + return sb.toString(); + } + } + + /** + * A test specification that is derived from a parent test specification, but will construct + * views sliced a particular way. In order to construct a test object, the factory of the parent + * is used, so that objects returned from here have the same type and root buffer value as the + * parent. However, {@link SlicedTestSpec#makePair()} returns a sliced view with the base + * exporter, and the reference material here is sliced correspondingly. + */ + static class SlicedTestSpec extends TestSpec { + + /** Number of consecutive bytes forming one item */ + final int itemsize; + /** Index in the parent object of item 0 of this slice */ + final int first; + /** The number of items that make up the slice. */ + final int count; + /** The item-index distance in the parent from one item to the next of this slice. */ + final int step; + + /** Byte-index in the original byte-array object of byte 0 of item 0 of the slice */ + final int start; + + /** + * A one-dimensional exporter with stride!=1 is restricted to give us a buffer only for + * these flag types. + */ + static final int[] strided1DFlags = {PyBUF.STRIDES, PyBUF.INDIRECT, PyBUF.FULL_RO}; + + /** To {@link #strided1DFlags} we can add any of these */ + static final int[] strided1DTassles = {0, PyBUF.FORMAT}; + + /** + * Construct a test specification based on a parent, but yielding objects and reference + * material whose values are related to those of the parent according to the slice + * specification. + * + * @param parent specification of byte buffer to slice + * @param itemsize number of consecutive bytes forming one item + * @param first byte-index in the parent of byte 0 of item 0 the result + * @param count number of items in the slice + * @param step byte-index increment in the parent between items + */ + SlicedTestSpec(TestSpec parent, int itemsize, int first, int count, int step) { + super(parent, parent.ref.slice(itemsize, first, count, step), new int[] {count}, + new int[1], strided1DFlags, strided1DTassles); + // It only seems to make sense for byte-array parent (or does all scale?) + if (parent.getItemsize() != 1) { + throw new IllegalArgumentException("Only byte-array parent supported"); + } + this.itemsize = itemsize; + // Write these down verbatim for subsequent call to getBufferSlice + this.first = first; + this.count = count; + this.step = step; + // But these must be calculated carefully + this.start = parent.getStart() + first * parent.getStride(); + this.strides[0] = step * parent.getStride(); + } + + /** + * {@inheritDoc} + *

+ * The size given in construction of a SlicedTestSpec. + */ + @Override + int getItemsize() { + return itemsize; + } + + /** + * {@inheritDoc} + *

+ * The start given in construction of a SlicedTestSpec is a start byte index + * specification, which could itself be striding on the underlying object's storage. + */ + @Override + int getStart() { + return start; + } + + /** + * {@inheritDoc} + *

+ * In SlicedTestSpec the returned pair are a new instance of the root object + * (to be the original exporter) created by + * + *

+         * pair = parent.makePair();
+         * obj = pair.obj;
+         * 
+ * and a sliced buffer view onto it created by + * + *
+         * view = pair.view.getBufferSlice(flags, first, count, step);
+         * 
+ * This view-slicing will apply recursively if the parent is a {@link SlicedTestSpec}, just + * as the slicing of reference material was iterated in construction. + */ + @Override + public ObjectAndView makePair() { + // Create a fresh test object and buffer view from the parent spec + ObjectAndView pair = parent.makePair(); + // Make a sliced view and release the parent + PyBuffer view = pair.view.getBufferSlice(flags, first, count, step); + // Controlled release of the parent buffer since pair is local + pair.view.release(); + return new ObjectAndView(pair.obj, view); + } + + } +} -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Sat Aug 27 16:00:31 2016 From: jython-checkins at python.org (jeff.allen) Date: Sat, 27 Aug 2016 20:00:31 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Fixes_failures_in_test=5Fsh?= =?utf-8?q?util_on_Windows_=232314=2E_Also_addresses_=232474=2E?= Message-ID: <20160827200031.63704.20105.685A9BE1@psf.io> https://hg.python.org/jython/rev/a8fef10e48a5 changeset: 7951:a8fef10e48a5 user: Jeff Allen date: Sat Aug 27 19:28:01 2016 +0100 summary: Fixes failures in test_shutil on Windows #2314. Also addresses #2474. The simple DOS read-only flag on a directory doesn't mean what Java means by read-only, it means "don't delete me". So Java always reports failure, but it makes most sense for chmod not to. files: NEWS | 1 + src/org/python/modules/posix/PosixModule.java | 19 +++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/NEWS b/NEWS --- a/NEWS +++ b/NEWS @@ -17,6 +17,7 @@ causes an infinite recursion - [ 2112 ] time.strptime() has different default year in Jython and CPython - [ 1767 ] Rich comparisons + - [ 2314 ] Failures in test_shutil on Windows New Features - Added uname function to posix module. The mostly Java-based implementation even diff --git a/src/org/python/modules/posix/PosixModule.java b/src/org/python/modules/posix/PosixModule.java --- a/src/org/python/modules/posix/PosixModule.java +++ b/src/org/python/modules/posix/PosixModule.java @@ -326,7 +326,8 @@ File f = absolutePath(path).toFile(); if (!f.exists()) { throw Py.OSError(Errno.ENOENT, path); - } else if (!f.setWritable(writable)) { + } else if (!f.isDirectory() && !f.setWritable(writable)) { + // The read-only flag of directories means "don't delete" not "you can't write" throw Py.OSError(Errno.EPERM, path); } } catch (SecurityException ex) { @@ -1043,7 +1044,9 @@ sysrelease = br.readLine(); // to end the process sanely in case we deal with some // implementation that emits additional new-lines: - while (br.readLine() != null); + while (br.readLine() != null) { + ; + } br.close(); if (p.waitFor() != 0) { sysrelease = ""; @@ -1081,7 +1084,9 @@ uname_nodename = br.readLine(); // to end the process sanely in case we deal with some // implementation that emits additional new-lines: - while (br.readLine() != null); + while (br.readLine() != null) { + ; + } br.close(); if (p.waitFor() != 0) { uname_nodename = ""; @@ -1103,7 +1108,9 @@ } // to end the process sanely in case we deal with some // implementation that emits additional new-lines: - while (br.readLine() != null); + while (br.readLine() != null) { + ; + } br.close(); if (p.waitFor() != 0) { // No fallback for sysver available @@ -1151,7 +1158,9 @@ uname_machine = br.readLine(); // to end the process sanely in case we deal with some // implementation that emits additional new-lines: - while (br.readLine() != null); + while (br.readLine() != null) { + ; + } br.close(); if (p.waitFor() != 0) { // To leverage os.arch-fallback: -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Mon Aug 29 08:05:47 2016 From: jython-checkins at python.org (darjus.loktevic) Date: Mon, 29 Aug 2016 12:05:47 +0000 Subject: [Jython-checkins] =?utf-8?b?anl0aG9uOiBfZ2V0X29wZW5zc2xfa2V5X21h?= =?utf-8?q?nager_permit_matching_of_the_RSA_private_key_to_any_cert_in?= Message-ID: <20160829120546.82493.18683.8F86C329@psf.io> https://hg.python.org/jython/rev/a758bc067952 changeset: 7952:a758bc067952 user: Darjus Loktevic date: Mon Aug 29 22:05:28 2016 +1000 summary: _get_openssl_key_manager permit matching of the RSA private key to any cert in the pem file. Workarounds issue #2516 files: Lib/_sslcerts.py | 19 +++++++++---------- 1 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Lib/_sslcerts.py b/Lib/_sslcerts.py --- a/Lib/_sslcerts.py +++ b/Lib/_sslcerts.py @@ -119,20 +119,19 @@ from _socket import SSLError, SSL_ERROR_SSL raise SSLError(SSL_ERROR_SSL, "PEM lib (No private key loaded)") - keys_match = False + keys_match, validateable_keys_found = False, False for cert in certs: # TODO works for RSA only for now - if not isinstance(cert.publicKey, RSAPublicKey) and isinstance(private_key, RSAPrivateCrtKey): - keys_match = True - continue + if isinstance(cert.publicKey, RSAPublicKey) and isinstance(private_key, RSAPrivateCrtKey): + validateable_keys_found = True - if cert.publicKey.getModulus() == private_key.getModulus() \ - and cert.publicKey.getPublicExponent() == private_key.getPublicExponent(): - keys_match = True - else: - keys_match = False + if validateable_keys_found: + if cert.publicKey.getModulus() == private_key.getModulus() \ + and cert.publicKey.getPublicExponent() == private_key.getPublicExponent(): + keys_match = True + break - if key_file is not None and not keys_match: + if key_file is not None and validateable_keys_found and not keys_match: from _socket import SSLError, SSL_ERROR_SSL raise SSLError(SSL_ERROR_SSL, "key values mismatch") -- Repository URL: https://hg.python.org/jython From jython-checkins at python.org Mon Aug 29 10:00:07 2016 From: jython-checkins at python.org (stefan.richthofer) Date: Mon, 29 Aug 2016 14:00:07 +0000 Subject: [Jython-checkins] =?utf-8?q?jython=3A_Updated_NEWS-file=2E?= Message-ID: <20160829140001.20291.65089.F04D6CC7@psf.io> https://hg.python.org/jython/rev/63c3d1b61721 changeset: 7953:63c3d1b61721 user: stefan.richthofer date: Mon Aug 29 15:59:09 2016 +0200 summary: Updated NEWS-file. files: NEWS | 20 ++++++++++++++------ 1 files changed, 14 insertions(+), 6 deletions(-) diff --git a/NEWS b/NEWS --- a/NEWS +++ b/NEWS @@ -2,9 +2,17 @@ For more details, please see https://hg.python.org/jython -Jython 2.7.1rc +Jython 2.7.1rc1 Bugs fixed - - [ 2488 ] Always join on subprocess coupler threads + - [ 2516 ] _get_open_ssl_key_manager tries to validate that the private and + public keys match, and is throwing an SSLError: + "key values mismatch" when provided with multiple certs (Root/CA/Cert) + - [ 2314 ] Failures in test_shutil on Windows + - [ 2488 ] Subprocess should always join corresponding coupler threads + - [ 2461 ] SSL Handshake fails for peers connected via IPV6 + - [ 2508 ] Jython non-blocking socket send() does not conform to Python's behavior. + - [ 2462 ] SSL Handshake never happens even if do_handshake_on_connect=True for serv + - [ 2487 ] PyType.fromClass deadlocks on slow systems (circleci for example) - [ 2480 ] Repeating from import results in reload - [ 2472 ] Importing simplejson fails with: 'NoneType' object has no attribute 'encode_basestring_ascii' @@ -17,18 +25,18 @@ causes an infinite recursion - [ 2112 ] time.strptime() has different default year in Jython and CPython - [ 1767 ] Rich comparisons - - [ 2314 ] Failures in test_shutil on Windows New Features - - Added uname function to posix module. The mostly Java-based implementation even - works to some extend on non-posix systems. Additional tweaks extend this to full - featured uname-functionality on Windows systems (usable via os.uname()). - Buffer API changes allow java.nio.ByteBuffer to provide the storage when a PyBuffer is exported. This is to support CPython extensions via JyNI, but has other uses too (including access to direct memory buffers from Python). There is no change at the Python level or for client code using PyBuffer via the "fully encapsulated" API. It risks breaking code that makes direct access to a byte array via PyBuffer, implements the PyBuffer interface, or extends implementation classes in org.python.core.buffer. + - Updated Netty to 4.1.4 + - Fixed platform.mac_ver to provide actual info on Mac OS similar to CPython behavior. + - Added uname function to posix module. The mostly Java-based implementation even + works to some extend on non-posix systems (e.g. Windows). Jython 2.7.1b3 Bugs fixed -- Repository URL: https://hg.python.org/jython