From f9228cc644735696df271d3e680d413360a44188 Mon Sep 17 00:00:00 2001 From: mikepk Date: Sun, 21 May 2017 00:55:59 -0600 Subject: [PATCH] First commit of spavro --- .gitignore | 28 + VERSION.txt | 1 + avro.egg-info/PKG-INFO | 11 + avro.egg-info/SOURCES.txt | 27 + avro.egg-info/dependency_links.txt | 1 + avro.egg-info/requires.txt | 3 + avro.egg-info/top_level.txt | 1 + build.sh | 60 + build.xml | 235 ++ dist/avro-_AVRO_VERSION_-py2.7.egg | Bin 0 -> 74069 bytes interop/py.avro | Bin 0 -> 1390 bytes ipc/HandshakeRequest.avsc | 11 + ipc/HandshakeResponse.avsc | 15 + ivy.xml | 24 + ivysettings.xml | 30 + lib/pyAntTasks-1.3-LICENSE.txt | 202 ++ lib/pyAntTasks-1.3.jar | Bin 0 -> 18788 bytes lib/simplejson/LICENSE.txt | 19 + lib/simplejson/__init__.py | 318 +++ lib/simplejson/_speedups.c | 2329 +++++++++++++++++ lib/simplejson/decoder.py | 354 +++ lib/simplejson/encoder.py | 440 ++++ lib/simplejson/scanner.py | 65 + lib/simplejson/tool.py | 37 + scripts/avro | 262 ++ setup.py | 63 + test/av_bench.py | 77 + test/gen_interop_data.py | 47 + test/mock_tether_parent.py | 95 + test/sample_http_client.py | 92 + test/sample_http_server.py | 79 + test/set_avro_test_path.py | 40 + test/test_datafile.py | 205 ++ test/test_datafile_interop.py | 42 + test/test_io.py | 341 +++ test/test_ipc.py | 40 + test/test_protocol.py | 439 ++++ test/test_schema.py | 495 ++++ test/test_script.py | 256 ++ test/test_tether_task.py | 116 + test/test_tether_task_runner.py | 191 ++ test/test_tether_word_count.py | 213 ++ test/txsample_http_client.py | 106 + test/txsample_http_server.py | 70 + test/word_count_task.py | 96 + testdata/data/schema-tests.txt | 192 ++ testdata/data/syncInMeta.avro | Bin 0 -> 22609 bytes testdata/data/test.avro12 | Bin 0 -> 3120 bytes testdata/data/weather-snappy.avro | Bin 0 -> 330 bytes testdata/data/weather-sorted.avro | Bin 0 -> 335 bytes testdata/data/weather.avro | Bin 0 -> 358 bytes testdata/data/weather.json | 5 + testdata/interop/bin/test_rpc_interop.sh | 83 + .../interop/rpc/add/onePlusOne/request.avro | Bin 0 -> 171 bytes .../interop/rpc/add/onePlusOne/response.avro | Bin 0 -> 75 bytes testdata/interop/rpc/echo/foo/request.avro | Bin 0 -> 458 bytes testdata/interop/rpc/echo/foo/response.avro | Bin 0 -> 390 bytes testdata/interop/rpc/hello/world/request.avro | Bin 0 -> 162 bytes .../interop/rpc/hello/world/response.avro | Bin 0 -> 89 bytes testdata/schemas/BulkData.avpr | 21 + testdata/schemas/FooBarSpecificRecord.avsc | 22 + testdata/schemas/contexts.avdl | 41 + testdata/schemas/echo.avdl | 32 + testdata/schemas/http.avdl | 66 + testdata/schemas/interop.avsc | 28 + testdata/schemas/mail.avpr | 26 + testdata/schemas/namespace.avpr | 28 + testdata/schemas/nestedNullable.avdl | 41 + testdata/schemas/reserved.avsc | 2 + testdata/schemas/schemaevolution.avdl | 55 + testdata/schemas/simple.avpr | 80 + testdata/schemas/social.avdl | 33 + testdata/schemas/specialtypes.avdl | 109 + testdata/schemas/stringables.avdl | 32 + testdata/schemas/weather.avsc | 8 + 75 files changed, 8480 insertions(+) create mode 100755 .gitignore create mode 100755 VERSION.txt create mode 100644 avro.egg-info/PKG-INFO create mode 100644 avro.egg-info/SOURCES.txt create mode 100644 avro.egg-info/dependency_links.txt create mode 100644 avro.egg-info/requires.txt create mode 100644 avro.egg-info/top_level.txt create mode 100755 build.sh create mode 100755 build.xml create mode 100644 dist/avro-_AVRO_VERSION_-py2.7.egg create mode 100644 interop/py.avro create mode 100755 ipc/HandshakeRequest.avsc create mode 100755 ipc/HandshakeResponse.avsc create mode 100755 ivy.xml create mode 100755 ivysettings.xml create mode 100755 lib/pyAntTasks-1.3-LICENSE.txt create mode 100755 lib/pyAntTasks-1.3.jar create mode 100755 lib/simplejson/LICENSE.txt create mode 100755 lib/simplejson/__init__.py create mode 100755 lib/simplejson/_speedups.c create mode 100755 lib/simplejson/decoder.py create mode 100755 lib/simplejson/encoder.py create mode 100755 lib/simplejson/scanner.py create mode 100755 lib/simplejson/tool.py create mode 100755 scripts/avro create mode 100755 setup.py create mode 100644 test/av_bench.py create mode 100644 test/gen_interop_data.py create mode 100644 test/mock_tether_parent.py create mode 100644 test/sample_http_client.py create mode 100644 test/sample_http_server.py create mode 100644 test/set_avro_test_path.py create mode 100644 test/test_datafile.py create mode 100644 test/test_datafile_interop.py create mode 100644 test/test_io.py create mode 100644 test/test_ipc.py create mode 100644 test/test_protocol.py create mode 100644 test/test_schema.py create mode 100644 test/test_script.py create mode 100644 test/test_tether_task.py create mode 100644 test/test_tether_task_runner.py create mode 100644 test/test_tether_word_count.py create mode 100644 test/txsample_http_client.py create mode 100644 test/txsample_http_server.py create mode 100644 test/word_count_task.py create mode 100755 testdata/data/schema-tests.txt create mode 100755 testdata/data/syncInMeta.avro create mode 100755 testdata/data/test.avro12 create mode 100755 testdata/data/weather-snappy.avro create mode 100755 testdata/data/weather-sorted.avro create mode 100755 testdata/data/weather.avro create mode 100755 testdata/data/weather.json create mode 100755 testdata/interop/bin/test_rpc_interop.sh create mode 100755 testdata/interop/rpc/add/onePlusOne/request.avro create mode 100755 testdata/interop/rpc/add/onePlusOne/response.avro create mode 100755 testdata/interop/rpc/echo/foo/request.avro create mode 100755 testdata/interop/rpc/echo/foo/response.avro create mode 100755 testdata/interop/rpc/hello/world/request.avro create mode 100755 testdata/interop/rpc/hello/world/response.avro create mode 100755 testdata/schemas/BulkData.avpr create mode 100755 testdata/schemas/FooBarSpecificRecord.avsc create mode 100755 testdata/schemas/contexts.avdl create mode 100755 testdata/schemas/echo.avdl create mode 100755 testdata/schemas/http.avdl create mode 100755 testdata/schemas/interop.avsc create mode 100755 testdata/schemas/mail.avpr create mode 100755 testdata/schemas/namespace.avpr create mode 100755 testdata/schemas/nestedNullable.avdl create mode 100755 testdata/schemas/reserved.avsc create mode 100755 testdata/schemas/schemaevolution.avdl create mode 100755 testdata/schemas/simple.avpr create mode 100755 testdata/schemas/social.avdl create mode 100755 testdata/schemas/specialtypes.avdl create mode 100755 testdata/schemas/stringables.avdl create mode 100755 testdata/schemas/weather.avsc diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..738dac5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Standard Python files +*.template.py +*.pyc +#*# +._* +*.sqlite +*.pyc +*.pid +*.log +tmp/ +src/ +build/ +logs/* + +# OSX file metadata +.DS_Store + +environment.py +environments/* + +# Unit test artifacts +.coverage_output.xml +.nose_output.xml +.coverage + +# other stuff +samples +output \ No newline at end of file diff --git a/VERSION.txt b/VERSION.txt new file mode 100755 index 0000000..06898e3 --- /dev/null +++ b/VERSION.txt @@ -0,0 +1 @@ +1.9.0-SNAPSHOT \ No newline at end of file diff --git a/avro.egg-info/PKG-INFO b/avro.egg-info/PKG-INFO new file mode 100644 index 0000000..f982d46 --- /dev/null +++ b/avro.egg-info/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 1.0 +Name: avro +Version: -AVRO-VERSION- +Summary: Avro is a serialization and RPC framework. +Home-page: http://avro.apache.org/ +Author: Apache Avro +Author-email: dev@avro.apache.org +License: Apache License 2.0 +Description: UNKNOWN +Keywords: avro serialization rpc +Platform: UNKNOWN diff --git a/avro.egg-info/SOURCES.txt b/avro.egg-info/SOURCES.txt new file mode 100644 index 0000000..a4d5afa --- /dev/null +++ b/avro.egg-info/SOURCES.txt @@ -0,0 +1,27 @@ +setup.py +./scripts/avro +avro.egg-info/PKG-INFO +avro.egg-info/SOURCES.txt +avro.egg-info/dependency_links.txt +avro.egg-info/requires.txt +avro.egg-info/top_level.txt +src/avro/LICENSE +src/avro/NOTICE +src/avro/__init__.py +src/avro/datafile.py +src/avro/io.py +src/avro/ipc.py +src/avro/protocol.py +src/avro/schema.py +src/avro/tool.py +src/avro/txipc.py +test/test_datafile.py +test/test_datafile_interop.py +test/test_io.py +test/test_ipc.py +test/test_protocol.py +test/test_schema.py +test/test_script.py +test/test_tether_task.py +test/test_tether_task_runner.py +test/test_tether_word_count.py \ No newline at end of file diff --git a/avro.egg-info/dependency_links.txt b/avro.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/avro.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/avro.egg-info/requires.txt b/avro.egg-info/requires.txt new file mode 100644 index 0000000..9f06150 --- /dev/null +++ b/avro.egg-info/requires.txt @@ -0,0 +1,3 @@ + +[snappy] +python-snappy diff --git a/avro.egg-info/top_level.txt b/avro.egg-info/top_level.txt new file mode 100644 index 0000000..240d563 --- /dev/null +++ b/avro.egg-info/top_level.txt @@ -0,0 +1 @@ +avro diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..0169b0b --- /dev/null +++ b/build.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e # exit on error + +function usage { + echo "Usage: $0 {test|dist|clean}" + exit 1 +} + +if [ $# -eq 0 ] +then + usage +fi + +if [ -f VERSION.txt ] +then + VERSION=`cat VERSION.txt` +else + VERSION=`cat ../../share/VERSION.txt` +fi + +for target in "$@" +do + +case "$target" in + test) + ant test + ;; + + dist) + ant dist + ;; + + clean) + ant clean + rm -rf userlogs/ + ;; + + *) + usage +esac + +done + +exit 0 diff --git a/build.xml b/build.xml new file mode 100755 index 0000000..0a549fd --- /dev/null +++ b/build.xml @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dist/avro-_AVRO_VERSION_-py2.7.egg b/dist/avro-_AVRO_VERSION_-py2.7.egg new file mode 100644 index 0000000000000000000000000000000000000000..48f4dabf381f9a063ef495fe878560b2b4ca9c3b GIT binary patch literal 74069 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 z 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 literal 0 HcmV?d00001 diff --git a/ipc/HandshakeRequest.avsc b/ipc/HandshakeRequest.avsc new file mode 100755 index 0000000..47f0256 --- /dev/null +++ b/ipc/HandshakeRequest.avsc @@ -0,0 +1,11 @@ +{ + "type": "record", + "name": "HandshakeRequest", "namespace":"org.apache.avro.ipc", + "fields": [ + {"name": "clientHash", + "type": {"type": "fixed", "name": "MD5", "size": 16}}, + {"name": "clientProtocol", "type": ["null", "string"]}, + {"name": "serverHash", "type": "MD5"}, + {"name": "meta", "type": ["null", {"type": "map", "values": "bytes"}]} + ] +} diff --git a/ipc/HandshakeResponse.avsc b/ipc/HandshakeResponse.avsc new file mode 100755 index 0000000..b54d16d --- /dev/null +++ b/ipc/HandshakeResponse.avsc @@ -0,0 +1,15 @@ +{ + "type": "record", + "name": "HandshakeResponse", "namespace": "org.apache.avro.ipc", + "fields": [ + {"name": "match", + "type": {"type": "enum", "name": "HandshakeMatch", + "symbols": ["BOTH", "CLIENT", "NONE"]}}, + {"name": "serverProtocol", + "type": ["null", "string"]}, + {"name": "serverHash", + "type": ["null", {"type": "fixed", "name": "MD5", "size": 16}]}, + {"name": "meta", + "type": ["null", {"type": "map", "values": "bytes"}]} + ] +} diff --git a/ivy.xml b/ivy.xml new file mode 100755 index 0000000..c37216c --- /dev/null +++ b/ivy.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/ivysettings.xml b/ivysettings.xml new file mode 100755 index 0000000..22104c7 --- /dev/null +++ b/ivysettings.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + diff --git a/lib/pyAntTasks-1.3-LICENSE.txt b/lib/pyAntTasks-1.3-LICENSE.txt new file mode 100755 index 0000000..d645695 --- /dev/null +++ b/lib/pyAntTasks-1.3-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/pyAntTasks-1.3.jar b/lib/pyAntTasks-1.3.jar new file mode 100755 index 0000000000000000000000000000000000000000..53a7877220fcfddee090158082bdf4d7b9ba635f GIT binary patch literal 18788 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 UN!*ele-K3Sddo`Pc4NN%02ZtHOaK4? literal 0 HcmV?d00001 diff --git a/lib/simplejson/LICENSE.txt b/lib/simplejson/LICENSE.txt new file mode 100755 index 0000000..ad95f29 --- /dev/null +++ b/lib/simplejson/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2006 Bob Ippolito + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/simplejson/__init__.py b/lib/simplejson/__init__.py new file mode 100755 index 0000000..d5b4d39 --- /dev/null +++ b/lib/simplejson/__init__.py @@ -0,0 +1,318 @@ +r"""JSON (JavaScript Object Notation) is a subset of +JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data +interchange format. + +:mod:`simplejson` exposes an API familiar to users of the standard library +:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained +version of the :mod:`json` library contained in Python 2.6, but maintains +compatibility with Python 2.4 and Python 2.5 and (currently) has +significant performance advantages, even without using the optional C +extension for speedups. + +Encoding basic Python object hierarchies:: + + >>> import simplejson as json + >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) + '["foo", {"bar": ["baz", null, 1.0, 2]}]' + >>> print json.dumps("\"foo\bar") + "\"foo\bar" + >>> print json.dumps(u'\u1234') + "\u1234" + >>> print json.dumps('\\') + "\\" + >>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) + {"a": 0, "b": 0, "c": 0} + >>> from StringIO import StringIO + >>> io = StringIO() + >>> json.dump(['streaming API'], io) + >>> io.getvalue() + '["streaming API"]' + +Compact encoding:: + + >>> import simplejson as json + >>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':')) + '[1,2,3,{"4":5,"6":7}]' + +Pretty printing:: + + >>> import simplejson as json + >>> s = json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=4) + >>> print '\n'.join([l.rstrip() for l in s.splitlines()]) + { + "4": 5, + "6": 7 + } + +Decoding JSON:: + + >>> import simplejson as json + >>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] + >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj + True + >>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar' + True + >>> from StringIO import StringIO + >>> io = StringIO('["streaming API"]') + >>> json.load(io)[0] == 'streaming API' + True + +Specializing JSON object decoding:: + + >>> import simplejson as json + >>> def as_complex(dct): + ... if '__complex__' in dct: + ... return complex(dct['real'], dct['imag']) + ... return dct + ... + >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}', + ... object_hook=as_complex) + (1+2j) + >>> import decimal + >>> json.loads('1.1', parse_float=decimal.Decimal) == decimal.Decimal('1.1') + True + +Specializing JSON object encoding:: + + >>> import simplejson as json + >>> def encode_complex(obj): + ... if isinstance(obj, complex): + ... return [obj.real, obj.imag] + ... raise TypeError(repr(o) + " is not JSON serializable") + ... + >>> json.dumps(2 + 1j, default=encode_complex) + '[2.0, 1.0]' + >>> json.JSONEncoder(default=encode_complex).encode(2 + 1j) + '[2.0, 1.0]' + >>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j)) + '[2.0, 1.0]' + + +Using simplejson.tool from the shell to validate and pretty-print:: + + $ echo '{"json":"obj"}' | python -m simplejson.tool + { + "json": "obj" + } + $ echo '{ 1.2:3.4}' | python -m simplejson.tool + Expecting property name: line 1 column 2 (char 2) +""" +__version__ = '2.0.9' +__all__ = [ + 'dump', 'dumps', 'load', 'loads', + 'JSONDecoder', 'JSONEncoder', +] + +__author__ = 'Bob Ippolito ' + +from decoder import JSONDecoder +from encoder import JSONEncoder + +_default_encoder = JSONEncoder( + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + encoding='utf-8', + default=None, +) + +def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, **kw): + """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a + ``.write()``-supporting file-like object). + + If ``skipkeys`` is true then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the some chunks written to ``fp`` + may be ``unicode`` instances, subject to normal Python ``str`` to + ``unicode`` coercion rules. Unless ``fp.write()`` explicitly + understands ``unicode`` (as in ``codecs.getwriter()``) this is likely + to cause an error. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) + in strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and object + members will be pretty-printed with that indent level. An indent level + of 0 will only insert newlines. ``None`` is the most compact representation. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not kw): + iterable = _default_encoder.iterencode(obj) + else: + if cls is None: + cls = JSONEncoder + iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, + default=default, **kw).iterencode(obj) + # could accelerate with writelines in some versions of Python, at + # a debuggability cost + for chunk in iterable: + fp.write(chunk) + + +def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, **kw): + """Serialize ``obj`` to a JSON formatted ``str``. + + If ``skipkeys`` is false then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the return value will be a + ``unicode`` instance subject to normal Python ``str`` to ``unicode`` + coercion rules instead of being escaped to an ASCII ``str``. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in + strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and + object members will be pretty-printed with that indent level. An indent + level of 0 will only insert newlines. ``None`` is the most compact + representation. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not kw): + return _default_encoder.encode(obj) + if cls is None: + cls = JSONEncoder + return cls( + skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, default=default, + **kw).encode(obj) + + +_default_decoder = JSONDecoder(encoding=None, object_hook=None) + + +def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, **kw): + """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing + a JSON document) to a Python object. + + If the contents of ``fp`` is encoded with an ASCII based encoding other + than utf-8 (e.g. latin-1), then an appropriate ``encoding`` name must + be specified. Encodings that are not ASCII based (such as UCS-2) are + not allowed, and should be wrapped with + ``codecs.getreader(fp)(encoding)``, or simply decoded to a ``unicode`` + object and passed to ``loads()`` + + ``object_hook`` is an optional function that will be called with the + result of any object literal decode (a ``dict``). The return value of + ``object_hook`` will be used instead of the ``dict``. This feature + can be used to implement custom decoders (e.g. JSON-RPC class hinting). + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + return loads(fp.read(), + encoding=encoding, cls=cls, object_hook=object_hook, + parse_float=parse_float, parse_int=parse_int, + parse_constant=parse_constant, **kw) + + +def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, **kw): + """Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON + document) to a Python object. + + If ``s`` is a ``str`` instance and is encoded with an ASCII based encoding + other than utf-8 (e.g. latin-1) then an appropriate ``encoding`` name + must be specified. Encodings that are not ASCII based (such as UCS-2) + are not allowed and should be decoded to ``unicode`` first. + + ``object_hook`` is an optional function that will be called with the + result of any object literal decode (a ``dict``). The return value of + ``object_hook`` will be used instead of the ``dict``. This feature + can be used to implement custom decoders (e.g. JSON-RPC class hinting). + + ``parse_float``, if specified, will be called with the string + of every JSON float to be decoded. By default this is equivalent to + float(num_str). This can be used to use another datatype or parser + for JSON floats (e.g. decimal.Decimal). + + ``parse_int``, if specified, will be called with the string + of every JSON int to be decoded. By default this is equivalent to + int(num_str). This can be used to use another datatype or parser + for JSON integers (e.g. float). + + ``parse_constant``, if specified, will be called with one of the + following strings: -Infinity, Infinity, NaN, null, true, false. + This can be used to raise an exception if invalid JSON numbers + are encountered. + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + if (cls is None and encoding is None and object_hook is None and + parse_int is None and parse_float is None and + parse_constant is None and not kw): + return _default_decoder.decode(s) + if cls is None: + cls = JSONDecoder + if object_hook is not None: + kw['object_hook'] = object_hook + if parse_float is not None: + kw['parse_float'] = parse_float + if parse_int is not None: + kw['parse_int'] = parse_int + if parse_constant is not None: + kw['parse_constant'] = parse_constant + return cls(encoding=encoding, **kw).decode(s) diff --git a/lib/simplejson/_speedups.c b/lib/simplejson/_speedups.c new file mode 100755 index 0000000..23b5f4a --- /dev/null +++ b/lib/simplejson/_speedups.c @@ -0,0 +1,2329 @@ +#include "Python.h" +#include "structmember.h" +#if PY_VERSION_HEX < 0x02060000 && !defined(Py_TYPE) +#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) +#endif +#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) +typedef int Py_ssize_t; +#define PY_SSIZE_T_MAX INT_MAX +#define PY_SSIZE_T_MIN INT_MIN +#define PyInt_FromSsize_t PyInt_FromLong +#define PyInt_AsSsize_t PyInt_AsLong +#endif +#ifndef Py_IS_FINITE +#define Py_IS_FINITE(X) (!Py_IS_INFINITY(X) && !Py_IS_NAN(X)) +#endif + +#ifdef __GNUC__ +#define UNUSED __attribute__((__unused__)) +#else +#define UNUSED +#endif + +#define DEFAULT_ENCODING "utf-8" + +#define PyScanner_Check(op) PyObject_TypeCheck(op, &PyScannerType) +#define PyScanner_CheckExact(op) (Py_TYPE(op) == &PyScannerType) +#define PyEncoder_Check(op) PyObject_TypeCheck(op, &PyEncoderType) +#define PyEncoder_CheckExact(op) (Py_TYPE(op) == &PyEncoderType) + +static PyTypeObject PyScannerType; +static PyTypeObject PyEncoderType; + +typedef struct _PyScannerObject { + PyObject_HEAD + PyObject *encoding; + PyObject *strict; + PyObject *object_hook; + PyObject *parse_float; + PyObject *parse_int; + PyObject *parse_constant; +} PyScannerObject; + +static PyMemberDef scanner_members[] = { + {"encoding", T_OBJECT, offsetof(PyScannerObject, encoding), READONLY, "encoding"}, + {"strict", T_OBJECT, offsetof(PyScannerObject, strict), READONLY, "strict"}, + {"object_hook", T_OBJECT, offsetof(PyScannerObject, object_hook), READONLY, "object_hook"}, + {"parse_float", T_OBJECT, offsetof(PyScannerObject, parse_float), READONLY, "parse_float"}, + {"parse_int", T_OBJECT, offsetof(PyScannerObject, parse_int), READONLY, "parse_int"}, + {"parse_constant", T_OBJECT, offsetof(PyScannerObject, parse_constant), READONLY, "parse_constant"}, + {NULL} +}; + +typedef struct _PyEncoderObject { + PyObject_HEAD + PyObject *markers; + PyObject *defaultfn; + PyObject *encoder; + PyObject *indent; + PyObject *key_separator; + PyObject *item_separator; + PyObject *sort_keys; + PyObject *skipkeys; + int fast_encode; + int allow_nan; +} PyEncoderObject; + +static PyMemberDef encoder_members[] = { + {"markers", T_OBJECT, offsetof(PyEncoderObject, markers), READONLY, "markers"}, + {"default", T_OBJECT, offsetof(PyEncoderObject, defaultfn), READONLY, "default"}, + {"encoder", T_OBJECT, offsetof(PyEncoderObject, encoder), READONLY, "encoder"}, + {"indent", T_OBJECT, offsetof(PyEncoderObject, indent), READONLY, "indent"}, + {"key_separator", T_OBJECT, offsetof(PyEncoderObject, key_separator), READONLY, "key_separator"}, + {"item_separator", T_OBJECT, offsetof(PyEncoderObject, item_separator), READONLY, "item_separator"}, + {"sort_keys", T_OBJECT, offsetof(PyEncoderObject, sort_keys), READONLY, "sort_keys"}, + {"skipkeys", T_OBJECT, offsetof(PyEncoderObject, skipkeys), READONLY, "skipkeys"}, + {NULL} +}; + +static Py_ssize_t +ascii_escape_char(Py_UNICODE c, char *output, Py_ssize_t chars); +static PyObject * +ascii_escape_unicode(PyObject *pystr); +static PyObject * +ascii_escape_str(PyObject *pystr); +static PyObject * +py_encode_basestring_ascii(PyObject* self UNUSED, PyObject *pystr); +void init_speedups(void); +static PyObject * +scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr); +static PyObject * +scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr); +static PyObject * +_build_rval_index_tuple(PyObject *rval, Py_ssize_t idx); +static PyObject * +scanner_new(PyTypeObject *type, PyObject *args, PyObject *kwds); +static int +scanner_init(PyObject *self, PyObject *args, PyObject *kwds); +static void +scanner_dealloc(PyObject *self); +static int +scanner_clear(PyObject *self); +static PyObject * +encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds); +static int +encoder_init(PyObject *self, PyObject *args, PyObject *kwds); +static void +encoder_dealloc(PyObject *self); +static int +encoder_clear(PyObject *self); +static int +encoder_listencode_list(PyEncoderObject *s, PyObject *rval, PyObject *seq, Py_ssize_t indent_level); +static int +encoder_listencode_obj(PyEncoderObject *s, PyObject *rval, PyObject *obj, Py_ssize_t indent_level); +static int +encoder_listencode_dict(PyEncoderObject *s, PyObject *rval, PyObject *dct, Py_ssize_t indent_level); +static PyObject * +_encoded_const(PyObject *const); +static void +raise_errmsg(char *msg, PyObject *s, Py_ssize_t end); +static PyObject * +encoder_encode_string(PyEncoderObject *s, PyObject *obj); +static int +_convertPyInt_AsSsize_t(PyObject *o, Py_ssize_t *size_ptr); +static PyObject * +_convertPyInt_FromSsize_t(Py_ssize_t *size_ptr); +static PyObject * +encoder_encode_float(PyEncoderObject *s, PyObject *obj); + +#define S_CHAR(c) (c >= ' ' && c <= '~' && c != '\\' && c != '"') +#define IS_WHITESPACE(c) (((c) == ' ') || ((c) == '\t') || ((c) == '\n') || ((c) == '\r')) + +#define MIN_EXPANSION 6 +#ifdef Py_UNICODE_WIDE +#define MAX_EXPANSION (2 * MIN_EXPANSION) +#else +#define MAX_EXPANSION MIN_EXPANSION +#endif + +static int +_convertPyInt_AsSsize_t(PyObject *o, Py_ssize_t *size_ptr) +{ + /* PyObject to Py_ssize_t converter */ + *size_ptr = PyInt_AsSsize_t(o); + if (*size_ptr == -1 && PyErr_Occurred()); + return 1; + return 0; +} + +static PyObject * +_convertPyInt_FromSsize_t(Py_ssize_t *size_ptr) +{ + /* Py_ssize_t to PyObject converter */ + return PyInt_FromSsize_t(*size_ptr); +} + +static Py_ssize_t +ascii_escape_char(Py_UNICODE c, char *output, Py_ssize_t chars) +{ + /* Escape unicode code point c to ASCII escape sequences + in char *output. output must have at least 12 bytes unused to + accommodate an escaped surrogate pair "\uXXXX\uXXXX" */ + output[chars++] = '\\'; + switch (c) { + case '\\': output[chars++] = (char)c; break; + case '"': output[chars++] = (char)c; break; + case '\b': output[chars++] = 'b'; break; + case '\f': output[chars++] = 'f'; break; + case '\n': output[chars++] = 'n'; break; + case '\r': output[chars++] = 'r'; break; + case '\t': output[chars++] = 't'; break; + default: +#ifdef Py_UNICODE_WIDE + if (c >= 0x10000) { + /* UTF-16 surrogate pair */ + Py_UNICODE v = c - 0x10000; + c = 0xd800 | ((v >> 10) & 0x3ff); + output[chars++] = 'u'; + output[chars++] = "0123456789abcdef"[(c >> 12) & 0xf]; + output[chars++] = "0123456789abcdef"[(c >> 8) & 0xf]; + output[chars++] = "0123456789abcdef"[(c >> 4) & 0xf]; + output[chars++] = "0123456789abcdef"[(c ) & 0xf]; + c = 0xdc00 | (v & 0x3ff); + output[chars++] = '\\'; + } +#endif + output[chars++] = 'u'; + output[chars++] = "0123456789abcdef"[(c >> 12) & 0xf]; + output[chars++] = "0123456789abcdef"[(c >> 8) & 0xf]; + output[chars++] = "0123456789abcdef"[(c >> 4) & 0xf]; + output[chars++] = "0123456789abcdef"[(c ) & 0xf]; + } + return chars; +} + +static PyObject * +ascii_escape_unicode(PyObject *pystr) +{ + /* Take a PyUnicode pystr and return a new ASCII-only escaped PyString */ + Py_ssize_t i; + Py_ssize_t input_chars; + Py_ssize_t output_size; + Py_ssize_t max_output_size; + Py_ssize_t chars; + PyObject *rval; + char *output; + Py_UNICODE *input_unicode; + + input_chars = PyUnicode_GET_SIZE(pystr); + input_unicode = PyUnicode_AS_UNICODE(pystr); + + /* One char input can be up to 6 chars output, estimate 4 of these */ + output_size = 2 + (MIN_EXPANSION * 4) + input_chars; + max_output_size = 2 + (input_chars * MAX_EXPANSION); + rval = PyString_FromStringAndSize(NULL, output_size); + if (rval == NULL) { + return NULL; + } + output = PyString_AS_STRING(rval); + chars = 0; + output[chars++] = '"'; + for (i = 0; i < input_chars; i++) { + Py_UNICODE c = input_unicode[i]; + if (S_CHAR(c)) { + output[chars++] = (char)c; + } + else { + chars = ascii_escape_char(c, output, chars); + } + if (output_size - chars < (1 + MAX_EXPANSION)) { + /* There's more than four, so let's resize by a lot */ + Py_ssize_t new_output_size = output_size * 2; + /* This is an upper bound */ + if (new_output_size > max_output_size) { + new_output_size = max_output_size; + } + /* Make sure that the output size changed before resizing */ + if (new_output_size != output_size) { + output_size = new_output_size; + if (_PyString_Resize(&rval, output_size) == -1) { + return NULL; + } + output = PyString_AS_STRING(rval); + } + } + } + output[chars++] = '"'; + if (_PyString_Resize(&rval, chars) == -1) { + return NULL; + } + return rval; +} + +static PyObject * +ascii_escape_str(PyObject *pystr) +{ + /* Take a PyString pystr and return a new ASCII-only escaped PyString */ + Py_ssize_t i; + Py_ssize_t input_chars; + Py_ssize_t output_size; + Py_ssize_t chars; + PyObject *rval; + char *output; + char *input_str; + + input_chars = PyString_GET_SIZE(pystr); + input_str = PyString_AS_STRING(pystr); + + /* Fast path for a string that's already ASCII */ + for (i = 0; i < input_chars; i++) { + Py_UNICODE c = (Py_UNICODE)(unsigned char)input_str[i]; + if (!S_CHAR(c)) { + /* If we have to escape something, scan the string for unicode */ + Py_ssize_t j; + for (j = i; j < input_chars; j++) { + c = (Py_UNICODE)(unsigned char)input_str[j]; + if (c > 0x7f) { + /* We hit a non-ASCII character, bail to unicode mode */ + PyObject *uni; + uni = PyUnicode_DecodeUTF8(input_str, input_chars, "strict"); + if (uni == NULL) { + return NULL; + } + rval = ascii_escape_unicode(uni); + Py_DECREF(uni); + return rval; + } + } + break; + } + } + + if (i == input_chars) { + /* Input is already ASCII */ + output_size = 2 + input_chars; + } + else { + /* One char input can be up to 6 chars output, estimate 4 of these */ + output_size = 2 + (MIN_EXPANSION * 4) + input_chars; + } + rval = PyString_FromStringAndSize(NULL, output_size); + if (rval == NULL) { + return NULL; + } + output = PyString_AS_STRING(rval); + output[0] = '"'; + + /* We know that everything up to i is ASCII already */ + chars = i + 1; + memcpy(&output[1], input_str, i); + + for (; i < input_chars; i++) { + Py_UNICODE c = (Py_UNICODE)(unsigned char)input_str[i]; + if (S_CHAR(c)) { + output[chars++] = (char)c; + } + else { + chars = ascii_escape_char(c, output, chars); + } + /* An ASCII char can't possibly expand to a surrogate! */ + if (output_size - chars < (1 + MIN_EXPANSION)) { + /* There's more than four, so let's resize by a lot */ + output_size *= 2; + if (output_size > 2 + (input_chars * MIN_EXPANSION)) { + output_size = 2 + (input_chars * MIN_EXPANSION); + } + if (_PyString_Resize(&rval, output_size) == -1) { + return NULL; + } + output = PyString_AS_STRING(rval); + } + } + output[chars++] = '"'; + if (_PyString_Resize(&rval, chars) == -1) { + return NULL; + } + return rval; +} + +static void +raise_errmsg(char *msg, PyObject *s, Py_ssize_t end) +{ + /* Use the Python function simplejson.decoder.errmsg to raise a nice + looking ValueError exception */ + static PyObject *errmsg_fn = NULL; + PyObject *pymsg; + if (errmsg_fn == NULL) { + PyObject *decoder = PyImport_ImportModule("simplejson.decoder"); + if (decoder == NULL) + return; + errmsg_fn = PyObject_GetAttrString(decoder, "errmsg"); + Py_DECREF(decoder); + if (errmsg_fn == NULL) + return; + } + pymsg = PyObject_CallFunction(errmsg_fn, "(zOO&)", msg, s, _convertPyInt_FromSsize_t, &end); + if (pymsg) { + PyErr_SetObject(PyExc_ValueError, pymsg); + Py_DECREF(pymsg); + } +} + +static PyObject * +join_list_unicode(PyObject *lst) +{ + /* return u''.join(lst) */ + static PyObject *joinfn = NULL; + if (joinfn == NULL) { + PyObject *ustr = PyUnicode_FromUnicode(NULL, 0); + if (ustr == NULL) + return NULL; + + joinfn = PyObject_GetAttrString(ustr, "join"); + Py_DECREF(ustr); + if (joinfn == NULL) + return NULL; + } + return PyObject_CallFunctionObjArgs(joinfn, lst, NULL); +} + +static PyObject * +join_list_string(PyObject *lst) +{ + /* return ''.join(lst) */ + static PyObject *joinfn = NULL; + if (joinfn == NULL) { + PyObject *ustr = PyString_FromStringAndSize(NULL, 0); + if (ustr == NULL) + return NULL; + + joinfn = PyObject_GetAttrString(ustr, "join"); + Py_DECREF(ustr); + if (joinfn == NULL) + return NULL; + } + return PyObject_CallFunctionObjArgs(joinfn, lst, NULL); +} + +static PyObject * +_build_rval_index_tuple(PyObject *rval, Py_ssize_t idx) { + /* return (rval, idx) tuple, stealing reference to rval */ + PyObject *tpl; + PyObject *pyidx; + /* + steal a reference to rval, returns (rval, idx) + */ + if (rval == NULL) { + return NULL; + } + pyidx = PyInt_FromSsize_t(idx); + if (pyidx == NULL) { + Py_DECREF(rval); + return NULL; + } + tpl = PyTuple_New(2); + if (tpl == NULL) { + Py_DECREF(pyidx); + Py_DECREF(rval); + return NULL; + } + PyTuple_SET_ITEM(tpl, 0, rval); + PyTuple_SET_ITEM(tpl, 1, pyidx); + return tpl; +} + +static PyObject * +scanstring_str(PyObject *pystr, Py_ssize_t end, char *encoding, int strict, Py_ssize_t *next_end_ptr) +{ + /* Read the JSON string from PyString pystr. + end is the index of the first character after the quote. + encoding is the encoding of pystr (must be an ASCII superset) + if strict is zero then literal control characters are allowed + *next_end_ptr is a return-by-reference index of the character + after the end quote + + Return value is a new PyString (if ASCII-only) or PyUnicode + */ + PyObject *rval; + Py_ssize_t len = PyString_GET_SIZE(pystr); + Py_ssize_t begin = end - 1; + Py_ssize_t next = begin; + int has_unicode = 0; + char *buf = PyString_AS_STRING(pystr); + PyObject *chunks = PyList_New(0); + if (chunks == NULL) { + goto bail; + } + if (end < 0 || len <= end) { + PyErr_SetString(PyExc_ValueError, "end is out of bounds"); + goto bail; + } + while (1) { + /* Find the end of the string or the next escape */ + Py_UNICODE c = 0; + PyObject *chunk = NULL; + for (next = end; next < len; next++) { + c = (unsigned char)buf[next]; + if (c == '"' || c == '\\') { + break; + } + else if (strict && c <= 0x1f) { + raise_errmsg("Invalid control character at", pystr, next); + goto bail; + } + else if (c > 0x7f) { + has_unicode = 1; + } + } + if (!(c == '"' || c == '\\')) { + raise_errmsg("Unterminated string starting at", pystr, begin); + goto bail; + } + /* Pick up this chunk if it's not zero length */ + if (next != end) { + PyObject *strchunk = PyString_FromStringAndSize(&buf[end], next - end); + if (strchunk == NULL) { + goto bail; + } + if (has_unicode) { + chunk = PyUnicode_FromEncodedObject(strchunk, encoding, NULL); + Py_DECREF(strchunk); + if (chunk == NULL) { + goto bail; + } + } + else { + chunk = strchunk; + } + if (PyList_Append(chunks, chunk)) { + Py_DECREF(chunk); + goto bail; + } + Py_DECREF(chunk); + } + next++; + if (c == '"') { + end = next; + break; + } + if (next == len) { + raise_errmsg("Unterminated string starting at", pystr, begin); + goto bail; + } + c = buf[next]; + if (c != 'u') { + /* Non-unicode backslash escapes */ + end = next + 1; + switch (c) { + case '"': break; + case '\\': break; + case '/': break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + default: c = 0; + } + if (c == 0) { + raise_errmsg("Invalid \\escape", pystr, end - 2); + goto bail; + } + } + else { + c = 0; + next++; + end = next + 4; + if (end >= len) { + raise_errmsg("Invalid \\uXXXX escape", pystr, next - 1); + goto bail; + } + /* Decode 4 hex digits */ + for (; next < end; next++) { + Py_UNICODE digit = buf[next]; + c <<= 4; + switch (digit) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + c |= (digit - '0'); break; + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': + c |= (digit - 'a' + 10); break; + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': + c |= (digit - 'A' + 10); break; + default: + raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); + goto bail; + } + } +#ifdef Py_UNICODE_WIDE + /* Surrogate pair */ + if ((c & 0xfc00) == 0xd800) { + Py_UNICODE c2 = 0; + if (end + 6 >= len) { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + if (buf[next++] != '\\' || buf[next++] != 'u') { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + end += 6; + /* Decode 4 hex digits */ + for (; next < end; next++) { + c2 <<= 4; + Py_UNICODE digit = buf[next]; + switch (digit) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + c2 |= (digit - '0'); break; + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': + c2 |= (digit - 'a' + 10); break; + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': + c2 |= (digit - 'A' + 10); break; + default: + raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); + goto bail; + } + } + if ((c2 & 0xfc00) != 0xdc00) { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00)); + } + else if ((c & 0xfc00) == 0xdc00) { + raise_errmsg("Unpaired low surrogate", pystr, end - 5); + goto bail; + } +#endif + } + if (c > 0x7f) { + has_unicode = 1; + } + if (has_unicode) { + chunk = PyUnicode_FromUnicode(&c, 1); + if (chunk == NULL) { + goto bail; + } + } + else { + char c_char = Py_CHARMASK(c); + chunk = PyString_FromStringAndSize(&c_char, 1); + if (chunk == NULL) { + goto bail; + } + } + if (PyList_Append(chunks, chunk)) { + Py_DECREF(chunk); + goto bail; + } + Py_DECREF(chunk); + } + + rval = join_list_string(chunks); + if (rval == NULL) { + goto bail; + } + Py_CLEAR(chunks); + *next_end_ptr = end; + return rval; +bail: + *next_end_ptr = -1; + Py_XDECREF(chunks); + return NULL; +} + + +static PyObject * +scanstring_unicode(PyObject *pystr, Py_ssize_t end, int strict, Py_ssize_t *next_end_ptr) +{ + /* Read the JSON string from PyUnicode pystr. + end is the index of the first character after the quote. + if strict is zero then literal control characters are allowed + *next_end_ptr is a return-by-reference index of the character + after the end quote + + Return value is a new PyUnicode + */ + PyObject *rval; + Py_ssize_t len = PyUnicode_GET_SIZE(pystr); + Py_ssize_t begin = end - 1; + Py_ssize_t next = begin; + const Py_UNICODE *buf = PyUnicode_AS_UNICODE(pystr); + PyObject *chunks = PyList_New(0); + if (chunks == NULL) { + goto bail; + } + if (end < 0 || len <= end) { + PyErr_SetString(PyExc_ValueError, "end is out of bounds"); + goto bail; + } + while (1) { + /* Find the end of the string or the next escape */ + Py_UNICODE c = 0; + PyObject *chunk = NULL; + for (next = end; next < len; next++) { + c = buf[next]; + if (c == '"' || c == '\\') { + break; + } + else if (strict && c <= 0x1f) { + raise_errmsg("Invalid control character at", pystr, next); + goto bail; + } + } + if (!(c == '"' || c == '\\')) { + raise_errmsg("Unterminated string starting at", pystr, begin); + goto bail; + } + /* Pick up this chunk if it's not zero length */ + if (next != end) { + chunk = PyUnicode_FromUnicode(&buf[end], next - end); + if (chunk == NULL) { + goto bail; + } + if (PyList_Append(chunks, chunk)) { + Py_DECREF(chunk); + goto bail; + } + Py_DECREF(chunk); + } + next++; + if (c == '"') { + end = next; + break; + } + if (next == len) { + raise_errmsg("Unterminated string starting at", pystr, begin); + goto bail; + } + c = buf[next]; + if (c != 'u') { + /* Non-unicode backslash escapes */ + end = next + 1; + switch (c) { + case '"': break; + case '\\': break; + case '/': break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + default: c = 0; + } + if (c == 0) { + raise_errmsg("Invalid \\escape", pystr, end - 2); + goto bail; + } + } + else { + c = 0; + next++; + end = next + 4; + if (end >= len) { + raise_errmsg("Invalid \\uXXXX escape", pystr, next - 1); + goto bail; + } + /* Decode 4 hex digits */ + for (; next < end; next++) { + Py_UNICODE digit = buf[next]; + c <<= 4; + switch (digit) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + c |= (digit - '0'); break; + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': + c |= (digit - 'a' + 10); break; + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': + c |= (digit - 'A' + 10); break; + default: + raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); + goto bail; + } + } +#ifdef Py_UNICODE_WIDE + /* Surrogate pair */ + if ((c & 0xfc00) == 0xd800) { + Py_UNICODE c2 = 0; + if (end + 6 >= len) { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + if (buf[next++] != '\\' || buf[next++] != 'u') { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + end += 6; + /* Decode 4 hex digits */ + for (; next < end; next++) { + c2 <<= 4; + Py_UNICODE digit = buf[next]; + switch (digit) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + c2 |= (digit - '0'); break; + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': + c2 |= (digit - 'a' + 10); break; + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': + c2 |= (digit - 'A' + 10); break; + default: + raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); + goto bail; + } + } + if ((c2 & 0xfc00) != 0xdc00) { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00)); + } + else if ((c & 0xfc00) == 0xdc00) { + raise_errmsg("Unpaired low surrogate", pystr, end - 5); + goto bail; + } +#endif + } + chunk = PyUnicode_FromUnicode(&c, 1); + if (chunk == NULL) { + goto bail; + } + if (PyList_Append(chunks, chunk)) { + Py_DECREF(chunk); + goto bail; + } + Py_DECREF(chunk); + } + + rval = join_list_unicode(chunks); + if (rval == NULL) { + goto bail; + } + Py_DECREF(chunks); + *next_end_ptr = end; + return rval; +bail: + *next_end_ptr = -1; + Py_XDECREF(chunks); + return NULL; +} + +PyDoc_STRVAR(pydoc_scanstring, + "scanstring(basestring, end, encoding, strict=True) -> (str, end)\n" + "\n" + "Scan the string s for a JSON string. End is the index of the\n" + "character in s after the quote that started the JSON string.\n" + "Unescapes all valid JSON string escape sequences and raises ValueError\n" + "on attempt to decode an invalid string. If strict is False then literal\n" + "control characters are allowed in the string.\n" + "\n" + "Returns a tuple of the decoded string and the index of the character in s\n" + "after the end quote." +); + +static PyObject * +py_scanstring(PyObject* self UNUSED, PyObject *args) +{ + PyObject *pystr; + PyObject *rval; + Py_ssize_t end; + Py_ssize_t next_end = -1; + char *encoding = NULL; + int strict = 1; + if (!PyArg_ParseTuple(args, "OO&|zi:scanstring", &pystr, _convertPyInt_AsSsize_t, &end, &encoding, &strict)) { + return NULL; + } + if (encoding == NULL) { + encoding = DEFAULT_ENCODING; + } + if (PyString_Check(pystr)) { + rval = scanstring_str(pystr, end, encoding, strict, &next_end); + } + else if (PyUnicode_Check(pystr)) { + rval = scanstring_unicode(pystr, end, strict, &next_end); + } + else { + PyErr_Format(PyExc_TypeError, + "first argument must be a string, not %.80s", + Py_TYPE(pystr)->tp_name); + return NULL; + } + return _build_rval_index_tuple(rval, next_end); +} + +PyDoc_STRVAR(pydoc_encode_basestring_ascii, + "encode_basestring_ascii(basestring) -> str\n" + "\n" + "Return an ASCII-only JSON representation of a Python string" +); + +static PyObject * +py_encode_basestring_ascii(PyObject* self UNUSED, PyObject *pystr) +{ + /* Return an ASCII-only JSON representation of a Python string */ + /* METH_O */ + if (PyString_Check(pystr)) { + return ascii_escape_str(pystr); + } + else if (PyUnicode_Check(pystr)) { + return ascii_escape_unicode(pystr); + } + else { + PyErr_Format(PyExc_TypeError, + "first argument must be a string, not %.80s", + Py_TYPE(pystr)->tp_name); + return NULL; + } +} + +static void +scanner_dealloc(PyObject *self) +{ + /* Deallocate scanner object */ + scanner_clear(self); + Py_TYPE(self)->tp_free(self); +} + +static int +scanner_traverse(PyObject *self, visitproc visit, void *arg) +{ + PyScannerObject *s; + assert(PyScanner_Check(self)); + s = (PyScannerObject *)self; + Py_VISIT(s->encoding); + Py_VISIT(s->strict); + Py_VISIT(s->object_hook); + Py_VISIT(s->parse_float); + Py_VISIT(s->parse_int); + Py_VISIT(s->parse_constant); + return 0; +} + +static int +scanner_clear(PyObject *self) +{ + PyScannerObject *s; + assert(PyScanner_Check(self)); + s = (PyScannerObject *)self; + Py_CLEAR(s->encoding); + Py_CLEAR(s->strict); + Py_CLEAR(s->object_hook); + Py_CLEAR(s->parse_float); + Py_CLEAR(s->parse_int); + Py_CLEAR(s->parse_constant); + return 0; +} + +static PyObject * +_parse_object_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON object from PyString pystr. + idx is the index of the first character after the opening curly brace. + *next_idx_ptr is a return-by-reference index to the first character after + the closing curly brace. + + Returns a new PyObject (usually a dict, but object_hook can change that) + */ + char *str = PyString_AS_STRING(pystr); + Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; + PyObject *rval = PyDict_New(); + PyObject *key = NULL; + PyObject *val = NULL; + char *encoding = PyString_AS_STRING(s->encoding); + int strict = PyObject_IsTrue(s->strict); + Py_ssize_t next_idx; + if (rval == NULL) + return NULL; + + /* skip whitespace after { */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* only loop if the object is non-empty */ + if (idx <= end_idx && str[idx] != '}') { + while (idx <= end_idx) { + /* read key */ + if (str[idx] != '"') { + raise_errmsg("Expecting property name", pystr, idx); + goto bail; + } + key = scanstring_str(pystr, idx + 1, encoding, strict, &next_idx); + if (key == NULL) + goto bail; + idx = next_idx; + + /* skip whitespace between key and : delimiter, read :, skip whitespace */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + if (idx > end_idx || str[idx] != ':') { + raise_errmsg("Expecting : delimiter", pystr, idx); + goto bail; + } + idx++; + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* read any JSON data type */ + val = scan_once_str(s, pystr, idx, &next_idx); + if (val == NULL) + goto bail; + + if (PyDict_SetItem(rval, key, val) == -1) + goto bail; + + Py_CLEAR(key); + Py_CLEAR(val); + idx = next_idx; + + /* skip whitespace before } or , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* bail if the object is closed or we didn't get the , delimiter */ + if (idx > end_idx) break; + if (str[idx] == '}') { + break; + } + else if (str[idx] != ',') { + raise_errmsg("Expecting , delimiter", pystr, idx); + goto bail; + } + idx++; + + /* skip whitespace after , delimiter */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + } + } + /* verify that idx < end_idx, str[idx] should be '}' */ + if (idx > end_idx || str[idx] != '}') { + raise_errmsg("Expecting object", pystr, end_idx); + goto bail; + } + /* if object_hook is not None: rval = object_hook(rval) */ + if (s->object_hook != Py_None) { + val = PyObject_CallFunctionObjArgs(s->object_hook, rval, NULL); + if (val == NULL) + goto bail; + Py_DECREF(rval); + rval = val; + val = NULL; + } + *next_idx_ptr = idx + 1; + return rval; +bail: + Py_XDECREF(key); + Py_XDECREF(val); + Py_DECREF(rval); + return NULL; +} + +static PyObject * +_parse_object_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON object from PyUnicode pystr. + idx is the index of the first character after the opening curly brace. + *next_idx_ptr is a return-by-reference index to the first character after + the closing curly brace. + + Returns a new PyObject (usually a dict, but object_hook can change that) + */ + Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); + Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; + PyObject *val = NULL; + PyObject *rval = PyDict_New(); + PyObject *key = NULL; + int strict = PyObject_IsTrue(s->strict); + Py_ssize_t next_idx; + if (rval == NULL) + return NULL; + + /* skip whitespace after { */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* only loop if the object is non-empty */ + if (idx <= end_idx && str[idx] != '}') { + while (idx <= end_idx) { + /* read key */ + if (str[idx] != '"') { + raise_errmsg("Expecting property name", pystr, idx); + goto bail; + } + key = scanstring_unicode(pystr, idx + 1, strict, &next_idx); + if (key == NULL) + goto bail; + idx = next_idx; + + /* skip whitespace between key and : delimiter, read :, skip whitespace */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + if (idx > end_idx || str[idx] != ':') { + raise_errmsg("Expecting : delimiter", pystr, idx); + goto bail; + } + idx++; + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* read any JSON term */ + val = scan_once_unicode(s, pystr, idx, &next_idx); + if (val == NULL) + goto bail; + + if (PyDict_SetItem(rval, key, val) == -1) + goto bail; + + Py_CLEAR(key); + Py_CLEAR(val); + idx = next_idx; + + /* skip whitespace before } or , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* bail if the object is closed or we didn't get the , delimiter */ + if (idx > end_idx) break; + if (str[idx] == '}') { + break; + } + else if (str[idx] != ',') { + raise_errmsg("Expecting , delimiter", pystr, idx); + goto bail; + } + idx++; + + /* skip whitespace after , delimiter */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + } + } + + /* verify that idx < end_idx, str[idx] should be '}' */ + if (idx > end_idx || str[idx] != '}') { + raise_errmsg("Expecting object", pystr, end_idx); + goto bail; + } + + /* if object_hook is not None: rval = object_hook(rval) */ + if (s->object_hook != Py_None) { + val = PyObject_CallFunctionObjArgs(s->object_hook, rval, NULL); + if (val == NULL) + goto bail; + Py_DECREF(rval); + rval = val; + val = NULL; + } + *next_idx_ptr = idx + 1; + return rval; +bail: + Py_XDECREF(key); + Py_XDECREF(val); + Py_DECREF(rval); + return NULL; +} + +static PyObject * +_parse_array_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON array from PyString pystr. + idx is the index of the first character after the opening brace. + *next_idx_ptr is a return-by-reference index to the first character after + the closing brace. + + Returns a new PyList + */ + char *str = PyString_AS_STRING(pystr); + Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; + PyObject *val = NULL; + PyObject *rval = PyList_New(0); + Py_ssize_t next_idx; + if (rval == NULL) + return NULL; + + /* skip whitespace after [ */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* only loop if the array is non-empty */ + if (idx <= end_idx && str[idx] != ']') { + while (idx <= end_idx) { + + /* read any JSON term and de-tuplefy the (rval, idx) */ + val = scan_once_str(s, pystr, idx, &next_idx); + if (val == NULL) + goto bail; + + if (PyList_Append(rval, val) == -1) + goto bail; + + Py_CLEAR(val); + idx = next_idx; + + /* skip whitespace between term and , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* bail if the array is closed or we didn't get the , delimiter */ + if (idx > end_idx) break; + if (str[idx] == ']') { + break; + } + else if (str[idx] != ',') { + raise_errmsg("Expecting , delimiter", pystr, idx); + goto bail; + } + idx++; + + /* skip whitespace after , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + } + } + + /* verify that idx < end_idx, str[idx] should be ']' */ + if (idx > end_idx || str[idx] != ']') { + raise_errmsg("Expecting object", pystr, end_idx); + goto bail; + } + *next_idx_ptr = idx + 1; + return rval; +bail: + Py_XDECREF(val); + Py_DECREF(rval); + return NULL; +} + +static PyObject * +_parse_array_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON array from PyString pystr. + idx is the index of the first character after the opening brace. + *next_idx_ptr is a return-by-reference index to the first character after + the closing brace. + + Returns a new PyList + */ + Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); + Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; + PyObject *val = NULL; + PyObject *rval = PyList_New(0); + Py_ssize_t next_idx; + if (rval == NULL) + return NULL; + + /* skip whitespace after [ */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* only loop if the array is non-empty */ + if (idx <= end_idx && str[idx] != ']') { + while (idx <= end_idx) { + + /* read any JSON term */ + val = scan_once_unicode(s, pystr, idx, &next_idx); + if (val == NULL) + goto bail; + + if (PyList_Append(rval, val) == -1) + goto bail; + + Py_CLEAR(val); + idx = next_idx; + + /* skip whitespace between term and , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* bail if the array is closed or we didn't get the , delimiter */ + if (idx > end_idx) break; + if (str[idx] == ']') { + break; + } + else if (str[idx] != ',') { + raise_errmsg("Expecting , delimiter", pystr, idx); + goto bail; + } + idx++; + + /* skip whitespace after , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + } + } + + /* verify that idx < end_idx, str[idx] should be ']' */ + if (idx > end_idx || str[idx] != ']') { + raise_errmsg("Expecting object", pystr, end_idx); + goto bail; + } + *next_idx_ptr = idx + 1; + return rval; +bail: + Py_XDECREF(val); + Py_DECREF(rval); + return NULL; +} + +static PyObject * +_parse_constant(PyScannerObject *s, char *constant, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON constant from PyString pystr. + constant is the constant string that was found + ("NaN", "Infinity", "-Infinity"). + idx is the index of the first character of the constant + *next_idx_ptr is a return-by-reference index to the first character after + the constant. + + Returns the result of parse_constant + */ + PyObject *cstr; + PyObject *rval; + /* constant is "NaN", "Infinity", or "-Infinity" */ + cstr = PyString_InternFromString(constant); + if (cstr == NULL) + return NULL; + + /* rval = parse_constant(constant) */ + rval = PyObject_CallFunctionObjArgs(s->parse_constant, cstr, NULL); + idx += PyString_GET_SIZE(cstr); + Py_DECREF(cstr); + *next_idx_ptr = idx; + return rval; +} + +static PyObject * +_match_number_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssize_t *next_idx_ptr) { + /* Read a JSON number from PyString pystr. + idx is the index of the first character of the number + *next_idx_ptr is a return-by-reference index to the first character after + the number. + + Returns a new PyObject representation of that number: + PyInt, PyLong, or PyFloat. + May return other types if parse_int or parse_float are set + */ + char *str = PyString_AS_STRING(pystr); + Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; + Py_ssize_t idx = start; + int is_float = 0; + PyObject *rval; + PyObject *numstr; + + /* read a sign if it's there, make sure it's not the end of the string */ + if (str[idx] == '-') { + idx++; + if (idx > end_idx) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + } + + /* read as many integer digits as we find as long as it doesn't start with 0 */ + if (str[idx] >= '1' && str[idx] <= '9') { + idx++; + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + } + /* if it starts with 0 we only expect one integer digit */ + else if (str[idx] == '0') { + idx++; + } + /* no integer digits, error */ + else { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + + /* if the next char is '.' followed by a digit then read all float digits */ + if (idx < end_idx && str[idx] == '.' && str[idx + 1] >= '0' && str[idx + 1] <= '9') { + is_float = 1; + idx += 2; + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + } + + /* if the next char is 'e' or 'E' then maybe read the exponent (or backtrack) */ + if (idx < end_idx && (str[idx] == 'e' || str[idx] == 'E')) { + + /* save the index of the 'e' or 'E' just in case we need to backtrack */ + Py_ssize_t e_start = idx; + idx++; + + /* read an exponent sign if present */ + if (idx < end_idx && (str[idx] == '-' || str[idx] == '+')) idx++; + + /* read all digits */ + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + + /* if we got a digit, then parse as float. if not, backtrack */ + if (str[idx - 1] >= '0' && str[idx - 1] <= '9') { + is_float = 1; + } + else { + idx = e_start; + } + } + + /* copy the section we determined to be a number */ + numstr = PyString_FromStringAndSize(&str[start], idx - start); + if (numstr == NULL) + return NULL; + if (is_float) { + /* parse as a float using a fast path if available, otherwise call user defined method */ + if (s->parse_float != (PyObject *)&PyFloat_Type) { + rval = PyObject_CallFunctionObjArgs(s->parse_float, numstr, NULL); + } + else { + rval = PyFloat_FromDouble(PyOS_ascii_atof(PyString_AS_STRING(numstr))); + } + } + else { + /* parse as an int using a fast path if available, otherwise call user defined method */ + if (s->parse_int != (PyObject *)&PyInt_Type) { + rval = PyObject_CallFunctionObjArgs(s->parse_int, numstr, NULL); + } + else { + rval = PyInt_FromString(PyString_AS_STRING(numstr), NULL, 10); + } + } + Py_DECREF(numstr); + *next_idx_ptr = idx; + return rval; +} + +static PyObject * +_match_number_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssize_t *next_idx_ptr) { + /* Read a JSON number from PyUnicode pystr. + idx is the index of the first character of the number + *next_idx_ptr is a return-by-reference index to the first character after + the number. + + Returns a new PyObject representation of that number: + PyInt, PyLong, or PyFloat. + May return other types if parse_int or parse_float are set + */ + Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); + Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; + Py_ssize_t idx = start; + int is_float = 0; + PyObject *rval; + PyObject *numstr; + + /* read a sign if it's there, make sure it's not the end of the string */ + if (str[idx] == '-') { + idx++; + if (idx > end_idx) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + } + + /* read as many integer digits as we find as long as it doesn't start with 0 */ + if (str[idx] >= '1' && str[idx] <= '9') { + idx++; + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + } + /* if it starts with 0 we only expect one integer digit */ + else if (str[idx] == '0') { + idx++; + } + /* no integer digits, error */ + else { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + + /* if the next char is '.' followed by a digit then read all float digits */ + if (idx < end_idx && str[idx] == '.' && str[idx + 1] >= '0' && str[idx + 1] <= '9') { + is_float = 1; + idx += 2; + while (idx < end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + } + + /* if the next char is 'e' or 'E' then maybe read the exponent (or backtrack) */ + if (idx < end_idx && (str[idx] == 'e' || str[idx] == 'E')) { + Py_ssize_t e_start = idx; + idx++; + + /* read an exponent sign if present */ + if (idx < end_idx && (str[idx] == '-' || str[idx] == '+')) idx++; + + /* read all digits */ + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + + /* if we got a digit, then parse as float. if not, backtrack */ + if (str[idx - 1] >= '0' && str[idx - 1] <= '9') { + is_float = 1; + } + else { + idx = e_start; + } + } + + /* copy the section we determined to be a number */ + numstr = PyUnicode_FromUnicode(&str[start], idx - start); + if (numstr == NULL) + return NULL; + if (is_float) { + /* parse as a float using a fast path if available, otherwise call user defined method */ + if (s->parse_float != (PyObject *)&PyFloat_Type) { + rval = PyObject_CallFunctionObjArgs(s->parse_float, numstr, NULL); + } + else { + rval = PyFloat_FromString(numstr, NULL); + } + } + else { + /* no fast path for unicode -> int, just call */ + rval = PyObject_CallFunctionObjArgs(s->parse_int, numstr, NULL); + } + Py_DECREF(numstr); + *next_idx_ptr = idx; + return rval; +} + +static PyObject * +scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) +{ + /* Read one JSON term (of any kind) from PyString pystr. + idx is the index of the first character of the term + *next_idx_ptr is a return-by-reference index to the first character after + the number. + + Returns a new PyObject representation of the term. + */ + char *str = PyString_AS_STRING(pystr); + Py_ssize_t length = PyString_GET_SIZE(pystr); + if (idx >= length) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + switch (str[idx]) { + case '"': + /* string */ + return scanstring_str(pystr, idx + 1, + PyString_AS_STRING(s->encoding), + PyObject_IsTrue(s->strict), + next_idx_ptr); + case '{': + /* object */ + return _parse_object_str(s, pystr, idx + 1, next_idx_ptr); + case '[': + /* array */ + return _parse_array_str(s, pystr, idx + 1, next_idx_ptr); + case 'n': + /* null */ + if ((idx + 3 < length) && str[idx + 1] == 'u' && str[idx + 2] == 'l' && str[idx + 3] == 'l') { + Py_INCREF(Py_None); + *next_idx_ptr = idx + 4; + return Py_None; + } + break; + case 't': + /* true */ + if ((idx + 3 < length) && str[idx + 1] == 'r' && str[idx + 2] == 'u' && str[idx + 3] == 'e') { + Py_INCREF(Py_True); + *next_idx_ptr = idx + 4; + return Py_True; + } + break; + case 'f': + /* false */ + if ((idx + 4 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'l' && str[idx + 3] == 's' && str[idx + 4] == 'e') { + Py_INCREF(Py_False); + *next_idx_ptr = idx + 5; + return Py_False; + } + break; + case 'N': + /* NaN */ + if ((idx + 2 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'N') { + return _parse_constant(s, "NaN", idx, next_idx_ptr); + } + break; + case 'I': + /* Infinity */ + if ((idx + 7 < length) && str[idx + 1] == 'n' && str[idx + 2] == 'f' && str[idx + 3] == 'i' && str[idx + 4] == 'n' && str[idx + 5] == 'i' && str[idx + 6] == 't' && str[idx + 7] == 'y') { + return _parse_constant(s, "Infinity", idx, next_idx_ptr); + } + break; + case '-': + /* -Infinity */ + if ((idx + 8 < length) && str[idx + 1] == 'I' && str[idx + 2] == 'n' && str[idx + 3] == 'f' && str[idx + 4] == 'i' && str[idx + 5] == 'n' && str[idx + 6] == 'i' && str[idx + 7] == 't' && str[idx + 8] == 'y') { + return _parse_constant(s, "-Infinity", idx, next_idx_ptr); + } + break; + } + /* Didn't find a string, object, array, or named constant. Look for a number. */ + return _match_number_str(s, pystr, idx, next_idx_ptr); +} + +static PyObject * +scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) +{ + /* Read one JSON term (of any kind) from PyUnicode pystr. + idx is the index of the first character of the term + *next_idx_ptr is a return-by-reference index to the first character after + the number. + + Returns a new PyObject representation of the term. + */ + Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); + Py_ssize_t length = PyUnicode_GET_SIZE(pystr); + if (idx >= length) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + switch (str[idx]) { + case '"': + /* string */ + return scanstring_unicode(pystr, idx + 1, + PyObject_IsTrue(s->strict), + next_idx_ptr); + case '{': + /* object */ + return _parse_object_unicode(s, pystr, idx + 1, next_idx_ptr); + case '[': + /* array */ + return _parse_array_unicode(s, pystr, idx + 1, next_idx_ptr); + case 'n': + /* null */ + if ((idx + 3 < length) && str[idx + 1] == 'u' && str[idx + 2] == 'l' && str[idx + 3] == 'l') { + Py_INCREF(Py_None); + *next_idx_ptr = idx + 4; + return Py_None; + } + break; + case 't': + /* true */ + if ((idx + 3 < length) && str[idx + 1] == 'r' && str[idx + 2] == 'u' && str[idx + 3] == 'e') { + Py_INCREF(Py_True); + *next_idx_ptr = idx + 4; + return Py_True; + } + break; + case 'f': + /* false */ + if ((idx + 4 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'l' && str[idx + 3] == 's' && str[idx + 4] == 'e') { + Py_INCREF(Py_False); + *next_idx_ptr = idx + 5; + return Py_False; + } + break; + case 'N': + /* NaN */ + if ((idx + 2 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'N') { + return _parse_constant(s, "NaN", idx, next_idx_ptr); + } + break; + case 'I': + /* Infinity */ + if ((idx + 7 < length) && str[idx + 1] == 'n' && str[idx + 2] == 'f' && str[idx + 3] == 'i' && str[idx + 4] == 'n' && str[idx + 5] == 'i' && str[idx + 6] == 't' && str[idx + 7] == 'y') { + return _parse_constant(s, "Infinity", idx, next_idx_ptr); + } + break; + case '-': + /* -Infinity */ + if ((idx + 8 < length) && str[idx + 1] == 'I' && str[idx + 2] == 'n' && str[idx + 3] == 'f' && str[idx + 4] == 'i' && str[idx + 5] == 'n' && str[idx + 6] == 'i' && str[idx + 7] == 't' && str[idx + 8] == 'y') { + return _parse_constant(s, "-Infinity", idx, next_idx_ptr); + } + break; + } + /* Didn't find a string, object, array, or named constant. Look for a number. */ + return _match_number_unicode(s, pystr, idx, next_idx_ptr); +} + +static PyObject * +scanner_call(PyObject *self, PyObject *args, PyObject *kwds) +{ + /* Python callable interface to scan_once_{str,unicode} */ + PyObject *pystr; + PyObject *rval; + Py_ssize_t idx; + Py_ssize_t next_idx = -1; + static char *kwlist[] = {"string", "idx", NULL}; + PyScannerObject *s; + assert(PyScanner_Check(self)); + s = (PyScannerObject *)self; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&:scan_once", kwlist, &pystr, _convertPyInt_AsSsize_t, &idx)) + return NULL; + + if (PyString_Check(pystr)) { + rval = scan_once_str(s, pystr, idx, &next_idx); + } + else if (PyUnicode_Check(pystr)) { + rval = scan_once_unicode(s, pystr, idx, &next_idx); + } + else { + PyErr_Format(PyExc_TypeError, + "first argument must be a string, not %.80s", + Py_TYPE(pystr)->tp_name); + return NULL; + } + return _build_rval_index_tuple(rval, next_idx); +} + +static PyObject * +scanner_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + PyScannerObject *s; + s = (PyScannerObject *)type->tp_alloc(type, 0); + if (s != NULL) { + s->encoding = NULL; + s->strict = NULL; + s->object_hook = NULL; + s->parse_float = NULL; + s->parse_int = NULL; + s->parse_constant = NULL; + } + return (PyObject *)s; +} + +static int +scanner_init(PyObject *self, PyObject *args, PyObject *kwds) +{ + /* Initialize Scanner object */ + PyObject *ctx; + static char *kwlist[] = {"context", NULL}; + PyScannerObject *s; + + assert(PyScanner_Check(self)); + s = (PyScannerObject *)self; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:make_scanner", kwlist, &ctx)) + return -1; + + /* PyString_AS_STRING is used on encoding */ + s->encoding = PyObject_GetAttrString(ctx, "encoding"); + if (s->encoding == Py_None) { + Py_DECREF(Py_None); + s->encoding = PyString_InternFromString(DEFAULT_ENCODING); + } + else if (PyUnicode_Check(s->encoding)) { + PyObject *tmp = PyUnicode_AsEncodedString(s->encoding, NULL, NULL); + Py_DECREF(s->encoding); + s->encoding = tmp; + } + if (s->encoding == NULL || !PyString_Check(s->encoding)) + goto bail; + + /* All of these will fail "gracefully" so we don't need to verify them */ + s->strict = PyObject_GetAttrString(ctx, "strict"); + if (s->strict == NULL) + goto bail; + s->object_hook = PyObject_GetAttrString(ctx, "object_hook"); + if (s->object_hook == NULL) + goto bail; + s->parse_float = PyObject_GetAttrString(ctx, "parse_float"); + if (s->parse_float == NULL) + goto bail; + s->parse_int = PyObject_GetAttrString(ctx, "parse_int"); + if (s->parse_int == NULL) + goto bail; + s->parse_constant = PyObject_GetAttrString(ctx, "parse_constant"); + if (s->parse_constant == NULL) + goto bail; + + return 0; + +bail: + Py_CLEAR(s->encoding); + Py_CLEAR(s->strict); + Py_CLEAR(s->object_hook); + Py_CLEAR(s->parse_float); + Py_CLEAR(s->parse_int); + Py_CLEAR(s->parse_constant); + return -1; +} + +PyDoc_STRVAR(scanner_doc, "JSON scanner object"); + +static +PyTypeObject PyScannerType = { + PyObject_HEAD_INIT(NULL) + 0, /* tp_internal */ + "simplejson._speedups.Scanner", /* tp_name */ + sizeof(PyScannerObject), /* tp_basicsize */ + 0, /* tp_itemsize */ + scanner_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + scanner_call, /* tp_call */ + 0, /* tp_str */ + 0,/* PyObject_GenericGetAttr, */ /* tp_getattro */ + 0,/* PyObject_GenericSetAttr, */ /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ + scanner_doc, /* tp_doc */ + scanner_traverse, /* tp_traverse */ + scanner_clear, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + scanner_members, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + scanner_init, /* tp_init */ + 0,/* PyType_GenericAlloc, */ /* tp_alloc */ + scanner_new, /* tp_new */ + 0,/* PyObject_GC_Del, */ /* tp_free */ +}; + +static PyObject * +encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + PyEncoderObject *s; + s = (PyEncoderObject *)type->tp_alloc(type, 0); + if (s != NULL) { + s->markers = NULL; + s->defaultfn = NULL; + s->encoder = NULL; + s->indent = NULL; + s->key_separator = NULL; + s->item_separator = NULL; + s->sort_keys = NULL; + s->skipkeys = NULL; + } + return (PyObject *)s; +} + +static int +encoder_init(PyObject *self, PyObject *args, PyObject *kwds) +{ + /* initialize Encoder object */ + static char *kwlist[] = {"markers", "default", "encoder", "indent", "key_separator", "item_separator", "sort_keys", "skipkeys", "allow_nan", NULL}; + + PyEncoderObject *s; + PyObject *allow_nan; + + assert(PyEncoder_Check(self)); + s = (PyEncoderObject *)self; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOOOOOO:make_encoder", kwlist, + &s->markers, &s->defaultfn, &s->encoder, &s->indent, &s->key_separator, &s->item_separator, &s->sort_keys, &s->skipkeys, &allow_nan)) + return -1; + + Py_INCREF(s->markers); + Py_INCREF(s->defaultfn); + Py_INCREF(s->encoder); + Py_INCREF(s->indent); + Py_INCREF(s->key_separator); + Py_INCREF(s->item_separator); + Py_INCREF(s->sort_keys); + Py_INCREF(s->skipkeys); + s->fast_encode = (PyCFunction_Check(s->encoder) && PyCFunction_GetFunction(s->encoder) == (PyCFunction)py_encode_basestring_ascii); + s->allow_nan = PyObject_IsTrue(allow_nan); + return 0; +} + +static PyObject * +encoder_call(PyObject *self, PyObject *args, PyObject *kwds) +{ + /* Python callable interface to encode_listencode_obj */ + static char *kwlist[] = {"obj", "_current_indent_level", NULL}; + PyObject *obj; + PyObject *rval; + Py_ssize_t indent_level; + PyEncoderObject *s; + assert(PyEncoder_Check(self)); + s = (PyEncoderObject *)self; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&:_iterencode", kwlist, + &obj, _convertPyInt_AsSsize_t, &indent_level)) + return NULL; + rval = PyList_New(0); + if (rval == NULL) + return NULL; + if (encoder_listencode_obj(s, rval, obj, indent_level)) { + Py_DECREF(rval); + return NULL; + } + return rval; +} + +static PyObject * +_encoded_const(PyObject *obj) +{ + /* Return the JSON string representation of None, True, False */ + if (obj == Py_None) { + static PyObject *s_null = NULL; + if (s_null == NULL) { + s_null = PyString_InternFromString("null"); + } + Py_INCREF(s_null); + return s_null; + } + else if (obj == Py_True) { + static PyObject *s_true = NULL; + if (s_true == NULL) { + s_true = PyString_InternFromString("true"); + } + Py_INCREF(s_true); + return s_true; + } + else if (obj == Py_False) { + static PyObject *s_false = NULL; + if (s_false == NULL) { + s_false = PyString_InternFromString("false"); + } + Py_INCREF(s_false); + return s_false; + } + else { + PyErr_SetString(PyExc_ValueError, "not a const"); + return NULL; + } +} + +static PyObject * +encoder_encode_float(PyEncoderObject *s, PyObject *obj) +{ + /* Return the JSON representation of a PyFloat */ + double i = PyFloat_AS_DOUBLE(obj); + if (!Py_IS_FINITE(i)) { + if (!s->allow_nan) { + PyErr_SetString(PyExc_ValueError, "Out of range float values are not JSON compliant"); + return NULL; + } + if (i > 0) { + return PyString_FromString("Infinity"); + } + else if (i < 0) { + return PyString_FromString("-Infinity"); + } + else { + return PyString_FromString("NaN"); + } + } + /* Use a better float format here? */ + return PyObject_Repr(obj); +} + +static PyObject * +encoder_encode_string(PyEncoderObject *s, PyObject *obj) +{ + /* Return the JSON representation of a string */ + if (s->fast_encode) + return py_encode_basestring_ascii(NULL, obj); + else + return PyObject_CallFunctionObjArgs(s->encoder, obj, NULL); +} + +static int +_steal_list_append(PyObject *lst, PyObject *stolen) +{ + /* Append stolen and then decrement its reference count */ + int rval = PyList_Append(lst, stolen); + Py_DECREF(stolen); + return rval; +} + +static int +encoder_listencode_obj(PyEncoderObject *s, PyObject *rval, PyObject *obj, Py_ssize_t indent_level) +{ + /* Encode Python object obj to a JSON term, rval is a PyList */ + PyObject *newobj; + int rv; + + if (obj == Py_None || obj == Py_True || obj == Py_False) { + PyObject *cstr = _encoded_const(obj); + if (cstr == NULL) + return -1; + return _steal_list_append(rval, cstr); + } + else if (PyString_Check(obj) || PyUnicode_Check(obj)) + { + PyObject *encoded = encoder_encode_string(s, obj); + if (encoded == NULL) + return -1; + return _steal_list_append(rval, encoded); + } + else if (PyInt_Check(obj) || PyLong_Check(obj)) { + PyObject *encoded = PyObject_Str(obj); + if (encoded == NULL) + return -1; + return _steal_list_append(rval, encoded); + } + else if (PyFloat_Check(obj)) { + PyObject *encoded = encoder_encode_float(s, obj); + if (encoded == NULL) + return -1; + return _steal_list_append(rval, encoded); + } + else if (PyList_Check(obj) || PyTuple_Check(obj)) { + return encoder_listencode_list(s, rval, obj, indent_level); + } + else if (PyDict_Check(obj)) { + return encoder_listencode_dict(s, rval, obj, indent_level); + } + else { + PyObject *ident = NULL; + if (s->markers != Py_None) { + int has_key; + ident = PyLong_FromVoidPtr(obj); + if (ident == NULL) + return -1; + has_key = PyDict_Contains(s->markers, ident); + if (has_key) { + if (has_key != -1) + PyErr_SetString(PyExc_ValueError, "Circular reference detected"); + Py_DECREF(ident); + return -1; + } + if (PyDict_SetItem(s->markers, ident, obj)) { + Py_DECREF(ident); + return -1; + } + } + newobj = PyObject_CallFunctionObjArgs(s->defaultfn, obj, NULL); + if (newobj == NULL) { + Py_XDECREF(ident); + return -1; + } + rv = encoder_listencode_obj(s, rval, newobj, indent_level); + Py_DECREF(newobj); + if (rv) { + Py_XDECREF(ident); + return -1; + } + if (ident != NULL) { + if (PyDict_DelItem(s->markers, ident)) { + Py_XDECREF(ident); + return -1; + } + Py_XDECREF(ident); + } + return rv; + } +} + +static int +encoder_listencode_dict(PyEncoderObject *s, PyObject *rval, PyObject *dct, Py_ssize_t indent_level) +{ + /* Encode Python dict dct a JSON term, rval is a PyList */ + static PyObject *open_dict = NULL; + static PyObject *close_dict = NULL; + static PyObject *empty_dict = NULL; + PyObject *kstr = NULL; + PyObject *ident = NULL; + PyObject *key, *value; + Py_ssize_t pos; + int skipkeys; + Py_ssize_t idx; + + if (open_dict == NULL || close_dict == NULL || empty_dict == NULL) { + open_dict = PyString_InternFromString("{"); + close_dict = PyString_InternFromString("}"); + empty_dict = PyString_InternFromString("{}"); + if (open_dict == NULL || close_dict == NULL || empty_dict == NULL) + return -1; + } + if (PyDict_Size(dct) == 0) + return PyList_Append(rval, empty_dict); + + if (s->markers != Py_None) { + int has_key; + ident = PyLong_FromVoidPtr(dct); + if (ident == NULL) + goto bail; + has_key = PyDict_Contains(s->markers, ident); + if (has_key) { + if (has_key != -1) + PyErr_SetString(PyExc_ValueError, "Circular reference detected"); + goto bail; + } + if (PyDict_SetItem(s->markers, ident, dct)) { + goto bail; + } + } + + if (PyList_Append(rval, open_dict)) + goto bail; + + if (s->indent != Py_None) { + /* TODO: DOES NOT RUN */ + indent_level += 1; + /* + newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) + separator = _item_separator + newline_indent + buf += newline_indent + */ + } + + /* TODO: C speedup not implemented for sort_keys */ + + pos = 0; + skipkeys = PyObject_IsTrue(s->skipkeys); + idx = 0; + while (PyDict_Next(dct, &pos, &key, &value)) { + PyObject *encoded; + + if (PyString_Check(key) || PyUnicode_Check(key)) { + Py_INCREF(key); + kstr = key; + } + else if (PyFloat_Check(key)) { + kstr = encoder_encode_float(s, key); + if (kstr == NULL) + goto bail; + } + else if (PyInt_Check(key) || PyLong_Check(key)) { + kstr = PyObject_Str(key); + if (kstr == NULL) + goto bail; + } + else if (key == Py_True || key == Py_False || key == Py_None) { + kstr = _encoded_const(key); + if (kstr == NULL) + goto bail; + } + else if (skipkeys) { + continue; + } + else { + /* TODO: include repr of key */ + PyErr_SetString(PyExc_ValueError, "keys must be a string"); + goto bail; + } + + if (idx) { + if (PyList_Append(rval, s->item_separator)) + goto bail; + } + + encoded = encoder_encode_string(s, kstr); + Py_CLEAR(kstr); + if (encoded == NULL) + goto bail; + if (PyList_Append(rval, encoded)) { + Py_DECREF(encoded); + goto bail; + } + Py_DECREF(encoded); + if (PyList_Append(rval, s->key_separator)) + goto bail; + if (encoder_listencode_obj(s, rval, value, indent_level)) + goto bail; + idx += 1; + } + if (ident != NULL) { + if (PyDict_DelItem(s->markers, ident)) + goto bail; + Py_CLEAR(ident); + } + if (s->indent != Py_None) { + /* TODO: DOES NOT RUN */ + indent_level -= 1; + /* + yield '\n' + (' ' * (_indent * _current_indent_level)) + */ + } + if (PyList_Append(rval, close_dict)) + goto bail; + return 0; + +bail: + Py_XDECREF(kstr); + Py_XDECREF(ident); + return -1; +} + + +static int +encoder_listencode_list(PyEncoderObject *s, PyObject *rval, PyObject *seq, Py_ssize_t indent_level) +{ + /* Encode Python list seq to a JSON term, rval is a PyList */ + static PyObject *open_array = NULL; + static PyObject *close_array = NULL; + static PyObject *empty_array = NULL; + PyObject *ident = NULL; + PyObject *s_fast = NULL; + Py_ssize_t num_items; + PyObject **seq_items; + Py_ssize_t i; + + if (open_array == NULL || close_array == NULL || empty_array == NULL) { + open_array = PyString_InternFromString("["); + close_array = PyString_InternFromString("]"); + empty_array = PyString_InternFromString("[]"); + if (open_array == NULL || close_array == NULL || empty_array == NULL) + return -1; + } + ident = NULL; + s_fast = PySequence_Fast(seq, "_iterencode_list needs a sequence"); + if (s_fast == NULL) + return -1; + num_items = PySequence_Fast_GET_SIZE(s_fast); + if (num_items == 0) { + Py_DECREF(s_fast); + return PyList_Append(rval, empty_array); + } + + if (s->markers != Py_None) { + int has_key; + ident = PyLong_FromVoidPtr(seq); + if (ident == NULL) + goto bail; + has_key = PyDict_Contains(s->markers, ident); + if (has_key) { + if (has_key != -1) + PyErr_SetString(PyExc_ValueError, "Circular reference detected"); + goto bail; + } + if (PyDict_SetItem(s->markers, ident, seq)) { + goto bail; + } + } + + seq_items = PySequence_Fast_ITEMS(s_fast); + if (PyList_Append(rval, open_array)) + goto bail; + if (s->indent != Py_None) { + /* TODO: DOES NOT RUN */ + indent_level += 1; + /* + newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) + separator = _item_separator + newline_indent + buf += newline_indent + */ + } + for (i = 0; i < num_items; i++) { + PyObject *obj = seq_items[i]; + if (i) { + if (PyList_Append(rval, s->item_separator)) + goto bail; + } + if (encoder_listencode_obj(s, rval, obj, indent_level)) + goto bail; + } + if (ident != NULL) { + if (PyDict_DelItem(s->markers, ident)) + goto bail; + Py_CLEAR(ident); + } + if (s->indent != Py_None) { + /* TODO: DOES NOT RUN */ + indent_level -= 1; + /* + yield '\n' + (' ' * (_indent * _current_indent_level)) + */ + } + if (PyList_Append(rval, close_array)) + goto bail; + Py_DECREF(s_fast); + return 0; + +bail: + Py_XDECREF(ident); + Py_DECREF(s_fast); + return -1; +} + +static void +encoder_dealloc(PyObject *self) +{ + /* Deallocate Encoder */ + encoder_clear(self); + Py_TYPE(self)->tp_free(self); +} + +static int +encoder_traverse(PyObject *self, visitproc visit, void *arg) +{ + PyEncoderObject *s; + assert(PyEncoder_Check(self)); + s = (PyEncoderObject *)self; + Py_VISIT(s->markers); + Py_VISIT(s->defaultfn); + Py_VISIT(s->encoder); + Py_VISIT(s->indent); + Py_VISIT(s->key_separator); + Py_VISIT(s->item_separator); + Py_VISIT(s->sort_keys); + Py_VISIT(s->skipkeys); + return 0; +} + +static int +encoder_clear(PyObject *self) +{ + /* Deallocate Encoder */ + PyEncoderObject *s; + assert(PyEncoder_Check(self)); + s = (PyEncoderObject *)self; + Py_CLEAR(s->markers); + Py_CLEAR(s->defaultfn); + Py_CLEAR(s->encoder); + Py_CLEAR(s->indent); + Py_CLEAR(s->key_separator); + Py_CLEAR(s->item_separator); + Py_CLEAR(s->sort_keys); + Py_CLEAR(s->skipkeys); + return 0; +} + +PyDoc_STRVAR(encoder_doc, "_iterencode(obj, _current_indent_level) -> iterable"); + +static +PyTypeObject PyEncoderType = { + PyObject_HEAD_INIT(NULL) + 0, /* tp_internal */ + "simplejson._speedups.Encoder", /* tp_name */ + sizeof(PyEncoderObject), /* tp_basicsize */ + 0, /* tp_itemsize */ + encoder_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + encoder_call, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ + encoder_doc, /* tp_doc */ + encoder_traverse, /* tp_traverse */ + encoder_clear, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + encoder_members, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + encoder_init, /* tp_init */ + 0, /* tp_alloc */ + encoder_new, /* tp_new */ + 0, /* tp_free */ +}; + +static PyMethodDef speedups_methods[] = { + {"encode_basestring_ascii", + (PyCFunction)py_encode_basestring_ascii, + METH_O, + pydoc_encode_basestring_ascii}, + {"scanstring", + (PyCFunction)py_scanstring, + METH_VARARGS, + pydoc_scanstring}, + {NULL, NULL, 0, NULL} +}; + +PyDoc_STRVAR(module_doc, +"simplejson speedups\n"); + +void +init_speedups(void) +{ + PyObject *m; + PyScannerType.tp_new = PyType_GenericNew; + if (PyType_Ready(&PyScannerType) < 0) + return; + PyEncoderType.tp_new = PyType_GenericNew; + if (PyType_Ready(&PyEncoderType) < 0) + return; + m = Py_InitModule3("_speedups", speedups_methods, module_doc); + Py_INCREF((PyObject*)&PyScannerType); + PyModule_AddObject(m, "make_scanner", (PyObject*)&PyScannerType); + Py_INCREF((PyObject*)&PyEncoderType); + PyModule_AddObject(m, "make_encoder", (PyObject*)&PyEncoderType); +} diff --git a/lib/simplejson/decoder.py b/lib/simplejson/decoder.py new file mode 100755 index 0000000..b769ea4 --- /dev/null +++ b/lib/simplejson/decoder.py @@ -0,0 +1,354 @@ +"""Implementation of JSONDecoder +""" +import re +import sys +import struct + +from simplejson.scanner import make_scanner +try: + from simplejson._speedups import scanstring as c_scanstring +except ImportError: + c_scanstring = None + +__all__ = ['JSONDecoder'] + +FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL + +def _floatconstants(): + _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') + if sys.byteorder != 'big': + _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] + nan, inf = struct.unpack('dd', _BYTES) + return nan, inf, -inf + +NaN, PosInf, NegInf = _floatconstants() + + +def linecol(doc, pos): + lineno = doc.count('\n', 0, pos) + 1 + if lineno == 1: + colno = pos + else: + colno = pos - doc.rindex('\n', 0, pos) + return lineno, colno + + +def errmsg(msg, doc, pos, end=None): + # Note that this function is called from _speedups + lineno, colno = linecol(doc, pos) + if end is None: + #fmt = '{0}: line {1} column {2} (char {3})' + #return fmt.format(msg, lineno, colno, pos) + fmt = '%s: line %d column %d (char %d)' + return fmt % (msg, lineno, colno, pos) + endlineno, endcolno = linecol(doc, end) + #fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})' + #return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end) + fmt = '%s: line %d column %d - line %d column %d (char %d - %d)' + return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end) + + +_CONSTANTS = { + '-Infinity': NegInf, + 'Infinity': PosInf, + 'NaN': NaN, +} + +STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) +BACKSLASH = { + '"': u'"', '\\': u'\\', '/': u'/', + 'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t', +} + +DEFAULT_ENCODING = "utf-8" + +def py_scanstring(s, end, encoding=None, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match): + """Scan the string s for a JSON string. End is the index of the + character in s after the quote that started the JSON string. + Unescapes all valid JSON string escape sequences and raises ValueError + on attempt to decode an invalid string. If strict is False then literal + control characters are allowed in the string. + + Returns a tuple of the decoded string and the index of the character in s + after the end quote.""" + if encoding is None: + encoding = DEFAULT_ENCODING + chunks = [] + _append = chunks.append + begin = end - 1 + while 1: + chunk = _m(s, end) + if chunk is None: + raise ValueError( + errmsg("Unterminated string starting at", s, begin)) + end = chunk.end() + content, terminator = chunk.groups() + # Content is contains zero or more unescaped string characters + if content: + if not isinstance(content, unicode): + content = unicode(content, encoding) + _append(content) + # Terminator is the end of string, a literal control character, + # or a backslash denoting that an escape sequence follows + if terminator == '"': + break + elif terminator != '\\': + if strict: + msg = "Invalid control character %r at" % (terminator,) + #msg = "Invalid control character {0!r} at".format(terminator) + raise ValueError(errmsg(msg, s, end)) + else: + _append(terminator) + continue + try: + esc = s[end] + except IndexError: + raise ValueError( + errmsg("Unterminated string starting at", s, begin)) + # If not a unicode escape sequence, must be in the lookup table + if esc != 'u': + try: + char = _b[esc] + except KeyError: + msg = "Invalid \\escape: " + repr(esc) + raise ValueError(errmsg(msg, s, end)) + end += 1 + else: + # Unicode escape sequence + esc = s[end + 1:end + 5] + next_end = end + 5 + if len(esc) != 4: + msg = "Invalid \\uXXXX escape" + raise ValueError(errmsg(msg, s, end)) + uni = int(esc, 16) + # Check for surrogate pair on UCS-4 systems + if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535: + msg = "Invalid \\uXXXX\\uXXXX surrogate pair" + if not s[end + 5:end + 7] == '\\u': + raise ValueError(errmsg(msg, s, end)) + esc2 = s[end + 7:end + 11] + if len(esc2) != 4: + raise ValueError(errmsg(msg, s, end)) + uni2 = int(esc2, 16) + uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) + next_end += 6 + char = unichr(uni) + end = next_end + # Append the unescaped character + _append(char) + return u''.join(chunks), end + + +# Use speedup if available +scanstring = c_scanstring or py_scanstring + +WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) +WHITESPACE_STR = ' \t\n\r' + +def JSONObject((s, end), encoding, strict, scan_once, object_hook, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + pairs = {} + # Use a slice to prevent IndexError from being raised, the following + # check will raise a more specific ValueError if the string is empty + nextchar = s[end:end + 1] + # Normally we expect nextchar == '"' + if nextchar != '"': + if nextchar in _ws: + end = _w(s, end).end() + nextchar = s[end:end + 1] + # Trivial empty object + if nextchar == '}': + return pairs, end + 1 + elif nextchar != '"': + raise ValueError(errmsg("Expecting property name", s, end)) + end += 1 + while True: + key, end = scanstring(s, end, encoding, strict) + + # To skip some function call overhead we optimize the fast paths where + # the JSON key separator is ": " or just ":". + if s[end:end + 1] != ':': + end = _w(s, end).end() + if s[end:end + 1] != ':': + raise ValueError(errmsg("Expecting : delimiter", s, end)) + + end += 1 + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + try: + value, end = scan_once(s, end) + except StopIteration: + raise ValueError(errmsg("Expecting object", s, end)) + pairs[key] = value + + try: + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + end += 1 + + if nextchar == '}': + break + elif nextchar != ',': + raise ValueError(errmsg("Expecting , delimiter", s, end - 1)) + + try: + nextchar = s[end] + if nextchar in _ws: + end += 1 + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + + end += 1 + if nextchar != '"': + raise ValueError(errmsg("Expecting property name", s, end - 1)) + + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end + +def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + values = [] + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + # Look-ahead for trivial empty array + if nextchar == ']': + return values, end + 1 + _append = values.append + while True: + try: + value, end = scan_once(s, end) + except StopIteration: + raise ValueError(errmsg("Expecting object", s, end)) + _append(value) + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + end += 1 + if nextchar == ']': + break + elif nextchar != ',': + raise ValueError(errmsg("Expecting , delimiter", s, end)) + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + return values, end + +class JSONDecoder(object): + """Simple JSON decoder + + Performs the following translations in decoding by default: + + +---------------+-------------------+ + | JSON | Python | + +===============+===================+ + | object | dict | + +---------------+-------------------+ + | array | list | + +---------------+-------------------+ + | string | unicode | + +---------------+-------------------+ + | number (int) | int, long | + +---------------+-------------------+ + | number (real) | float | + +---------------+-------------------+ + | true | True | + +---------------+-------------------+ + | false | False | + +---------------+-------------------+ + | null | None | + +---------------+-------------------+ + + It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as + their corresponding ``float`` values, which is outside the JSON spec. + + """ + + def __init__(self, encoding=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, strict=True): + """``encoding`` determines the encoding used to interpret any ``str`` + objects decoded by this instance (utf-8 by default). It has no + effect when decoding ``unicode`` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as ``unicode``. + + ``object_hook``, if specified, will be called with the result + of every JSON object decoded and its return value will be used in + place of the given ``dict``. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + ``parse_float``, if specified, will be called with the string + of every JSON float to be decoded. By default this is equivalent to + float(num_str). This can be used to use another datatype or parser + for JSON floats (e.g. decimal.Decimal). + + ``parse_int``, if specified, will be called with the string + of every JSON int to be decoded. By default this is equivalent to + int(num_str). This can be used to use another datatype or parser + for JSON integers (e.g. float). + + ``parse_constant``, if specified, will be called with one of the + following strings: -Infinity, Infinity, NaN. + This can be used to raise an exception if invalid JSON numbers + are encountered. + + """ + self.encoding = encoding + self.object_hook = object_hook + self.parse_float = parse_float or float + self.parse_int = parse_int or int + self.parse_constant = parse_constant or _CONSTANTS.__getitem__ + self.strict = strict + self.parse_object = JSONObject + self.parse_array = JSONArray + self.parse_string = scanstring + self.scan_once = make_scanner(self) + + def decode(self, s, _w=WHITESPACE.match): + """Return the Python representation of ``s`` (a ``str`` or ``unicode`` + instance containing a JSON document) + + """ + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + end = _w(s, end).end() + if end != len(s): + raise ValueError(errmsg("Extra data", s, end, len(s))) + return obj + + def raw_decode(self, s, idx=0): + """Decode a JSON document from ``s`` (a ``str`` or ``unicode`` beginning + with a JSON document) and return a 2-tuple of the Python + representation and the index in ``s`` where the document ended. + + This can be used to decode a JSON document from a string that may + have extraneous data at the end. + + """ + try: + obj, end = self.scan_once(s, idx) + except StopIteration: + raise ValueError("No JSON object could be decoded") + return obj, end diff --git a/lib/simplejson/encoder.py b/lib/simplejson/encoder.py new file mode 100755 index 0000000..cf58290 --- /dev/null +++ b/lib/simplejson/encoder.py @@ -0,0 +1,440 @@ +"""Implementation of JSONEncoder +""" +import re + +try: + from simplejson._speedups import encode_basestring_ascii as c_encode_basestring_ascii +except ImportError: + c_encode_basestring_ascii = None +try: + from simplejson._speedups import make_encoder as c_make_encoder +except ImportError: + c_make_encoder = None + +ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]') +ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') +HAS_UTF8 = re.compile(r'[\x80-\xff]') +ESCAPE_DCT = { + '\\': '\\\\', + '"': '\\"', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', +} +for i in range(0x20): + #ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i)) + ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) + +# Assume this produces an infinity on all machines (probably not guaranteed) +INFINITY = float('1e66666') +FLOAT_REPR = repr + +def encode_basestring(s): + """Return a JSON representation of a Python string + + """ + def replace(match): + return ESCAPE_DCT[match.group(0)] + return '"' + ESCAPE.sub(replace, s) + '"' + + +def py_encode_basestring_ascii(s): + """Return an ASCII-only JSON representation of a Python string + + """ + if isinstance(s, str) and HAS_UTF8.search(s) is not None: + s = s.decode('utf-8') + def replace(match): + s = match.group(0) + try: + return ESCAPE_DCT[s] + except KeyError: + n = ord(s) + if n < 0x10000: + #return '\\u{0:04x}'.format(n) + return '\\u%04x' % (n,) + else: + # surrogate pair + n -= 0x10000 + s1 = 0xd800 | ((n >> 10) & 0x3ff) + s2 = 0xdc00 | (n & 0x3ff) + #return '\\u{0:04x}\\u{1:04x}'.format(s1, s2) + return '\\u%04x\\u%04x' % (s1, s2) + return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' + + +encode_basestring_ascii = c_encode_basestring_ascii or py_encode_basestring_ascii + +class JSONEncoder(object): + """Extensible JSON encoder for Python data structures. + + Supports the following objects and types by default: + + +-------------------+---------------+ + | Python | JSON | + +===================+===============+ + | dict | object | + +-------------------+---------------+ + | list, tuple | array | + +-------------------+---------------+ + | str, unicode | string | + +-------------------+---------------+ + | int, long, float | number | + +-------------------+---------------+ + | True | true | + +-------------------+---------------+ + | False | false | + +-------------------+---------------+ + | None | null | + +-------------------+---------------+ + + To extend this to recognize other objects, subclass and implement a + ``.default()`` method with another method that returns a serializable + object for ``o`` if possible, otherwise it should call the superclass + implementation (to raise ``TypeError``). + + """ + item_separator = ', ' + key_separator = ': ' + def __init__(self, skipkeys=False, ensure_ascii=True, + check_circular=True, allow_nan=True, sort_keys=False, + indent=None, separators=None, encoding='utf-8', default=None): + """Constructor for JSONEncoder, with sensible defaults. + + If skipkeys is false, then it is a TypeError to attempt + encoding of keys that are not str, int, long, float or None. If + skipkeys is True, such items are simply skipped. + + If ensure_ascii is true, the output is guaranteed to be str + objects with all incoming unicode characters escaped. If + ensure_ascii is false, the output will be unicode object. + + If check_circular is true, then lists, dicts, and custom encoded + objects will be checked for circular references during encoding to + prevent an infinite recursion (which would cause an OverflowError). + Otherwise, no such check takes place. + + If allow_nan is true, then NaN, Infinity, and -Infinity will be + encoded as such. This behavior is not JSON specification compliant, + but is consistent with most JavaScript based encoders and decoders. + Otherwise, it will be a ValueError to encode such floats. + + If sort_keys is true, then the output of dictionaries will be + sorted by key; this is useful for regression tests to ensure + that JSON serializations can be compared on a day-to-day basis. + + If indent is a non-negative integer, then JSON array + elements and object members will be pretty-printed with that + indent level. An indent level of 0 will only insert newlines. + None is the most compact representation. + + If specified, separators should be a (item_separator, key_separator) + tuple. The default is (', ', ': '). To get the most compact JSON + representation you should specify (',', ':') to eliminate whitespace. + + If specified, default is a function that gets called for objects + that can't otherwise be serialized. It should return a JSON encodable + version of the object or raise a ``TypeError``. + + If encoding is not None, then all input strings will be + transformed into unicode using that encoding prior to JSON-encoding. + The default is UTF-8. + + """ + + self.skipkeys = skipkeys + self.ensure_ascii = ensure_ascii + self.check_circular = check_circular + self.allow_nan = allow_nan + self.sort_keys = sort_keys + self.indent = indent + if separators is not None: + self.item_separator, self.key_separator = separators + if default is not None: + self.default = default + self.encoding = encoding + + def default(self, o): + """Implement this method in a subclass such that it returns + a serializable object for ``o``, or calls the base implementation + (to raise a ``TypeError``). + + For example, to support arbitrary iterators, you could + implement default like this:: + + def default(self, o): + try: + iterable = iter(o) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, o) + + """ + raise TypeError(repr(o) + " is not JSON serializable") + + def encode(self, o): + """Return a JSON string representation of a Python data structure. + + >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) + '{"foo": ["bar", "baz"]}' + + """ + # This is for extremely simple cases and benchmarks. + if isinstance(o, basestring): + if isinstance(o, str): + _encoding = self.encoding + if (_encoding is not None + and not (_encoding == 'utf-8')): + o = o.decode(_encoding) + if self.ensure_ascii: + return encode_basestring_ascii(o) + else: + return encode_basestring(o) + # This doesn't pass the iterator directly to ''.join() because the + # exceptions aren't as detailed. The list call should be roughly + # equivalent to the PySequence_Fast that ''.join() would do. + chunks = self.iterencode(o, _one_shot=True) + if not isinstance(chunks, (list, tuple)): + chunks = list(chunks) + return ''.join(chunks) + + def iterencode(self, o, _one_shot=False): + """Encode the given object and yield each string + representation as available. + + For example:: + + for chunk in JSONEncoder().iterencode(bigobject): + mysocket.write(chunk) + + """ + if self.check_circular: + markers = {} + else: + markers = None + if self.ensure_ascii: + _encoder = encode_basestring_ascii + else: + _encoder = encode_basestring + if self.encoding != 'utf-8': + def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding): + if isinstance(o, str): + o = o.decode(_encoding) + return _orig_encoder(o) + + def floatstr(o, allow_nan=self.allow_nan, _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY): + # Check for specials. Note that this type of test is processor- and/or + # platform-specific, so do tests which don't depend on the internals. + + if o != o: + text = 'NaN' + elif o == _inf: + text = 'Infinity' + elif o == _neginf: + text = '-Infinity' + else: + return _repr(o) + + if not allow_nan: + raise ValueError( + "Out of range float values are not JSON compliant: " + + repr(o)) + + return text + + + if _one_shot and c_make_encoder is not None and not self.indent and not self.sort_keys: + _iterencode = c_make_encoder( + markers, self.default, _encoder, self.indent, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, self.allow_nan) + else: + _iterencode = _make_iterencode( + markers, self.default, _encoder, self.indent, floatstr, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, _one_shot) + return _iterencode(o, 0) + +def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, + ## HACK: hand-optimized bytecode; turn globals into locals + False=False, + True=True, + ValueError=ValueError, + basestring=basestring, + dict=dict, + float=float, + id=id, + int=int, + isinstance=isinstance, + list=list, + long=long, + str=str, + tuple=tuple, + ): + + def _iterencode_list(lst, _current_indent_level): + if not lst: + yield '[]' + return + if markers is not None: + markerid = id(lst) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = lst + buf = '[' + if _indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) + separator = _item_separator + newline_indent + buf += newline_indent + else: + newline_indent = None + separator = _item_separator + first = True + for value in lst: + if first: + first = False + else: + buf = separator + if isinstance(value, basestring): + yield buf + _encoder(value) + elif value is None: + yield buf + 'null' + elif value is True: + yield buf + 'true' + elif value is False: + yield buf + 'false' + elif isinstance(value, (int, long)): + yield buf + str(value) + elif isinstance(value, float): + yield buf + _floatstr(value) + else: + yield buf + if isinstance(value, (list, tuple)): + chunks = _iterencode_list(value, _current_indent_level) + elif isinstance(value, dict): + chunks = _iterencode_dict(value, _current_indent_level) + else: + chunks = _iterencode(value, _current_indent_level) + for chunk in chunks: + yield chunk + if newline_indent is not None: + _current_indent_level -= 1 + yield '\n' + (' ' * (_indent * _current_indent_level)) + yield ']' + if markers is not None: + del markers[markerid] + + def _iterencode_dict(dct, _current_indent_level): + if not dct: + yield '{}' + return + if markers is not None: + markerid = id(dct) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = dct + yield '{' + if _indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) + item_separator = _item_separator + newline_indent + yield newline_indent + else: + newline_indent = None + item_separator = _item_separator + first = True + if _sort_keys: + items = dct.items() + items.sort(key=lambda kv: kv[0]) + else: + items = dct.iteritems() + for key, value in items: + if isinstance(key, basestring): + pass + # JavaScript is weakly typed for these, so it makes sense to + # also allow them. Many encoders seem to do something like this. + elif isinstance(key, float): + key = _floatstr(key) + elif key is True: + key = 'true' + elif key is False: + key = 'false' + elif key is None: + key = 'null' + elif isinstance(key, (int, long)): + key = str(key) + elif _skipkeys: + continue + else: + raise TypeError("key " + repr(key) + " is not a string") + if first: + first = False + else: + yield item_separator + yield _encoder(key) + yield _key_separator + if isinstance(value, basestring): + yield _encoder(value) + elif value is None: + yield 'null' + elif value is True: + yield 'true' + elif value is False: + yield 'false' + elif isinstance(value, (int, long)): + yield str(value) + elif isinstance(value, float): + yield _floatstr(value) + else: + if isinstance(value, (list, tuple)): + chunks = _iterencode_list(value, _current_indent_level) + elif isinstance(value, dict): + chunks = _iterencode_dict(value, _current_indent_level) + else: + chunks = _iterencode(value, _current_indent_level) + for chunk in chunks: + yield chunk + if newline_indent is not None: + _current_indent_level -= 1 + yield '\n' + (' ' * (_indent * _current_indent_level)) + yield '}' + if markers is not None: + del markers[markerid] + + def _iterencode(o, _current_indent_level): + if isinstance(o, basestring): + yield _encoder(o) + elif o is None: + yield 'null' + elif o is True: + yield 'true' + elif o is False: + yield 'false' + elif isinstance(o, (int, long)): + yield str(o) + elif isinstance(o, float): + yield _floatstr(o) + elif isinstance(o, (list, tuple)): + for chunk in _iterencode_list(o, _current_indent_level): + yield chunk + elif isinstance(o, dict): + for chunk in _iterencode_dict(o, _current_indent_level): + yield chunk + else: + if markers is not None: + markerid = id(o) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = o + o = _default(o) + for chunk in _iterencode(o, _current_indent_level): + yield chunk + if markers is not None: + del markers[markerid] + + return _iterencode diff --git a/lib/simplejson/scanner.py b/lib/simplejson/scanner.py new file mode 100755 index 0000000..adbc6ec --- /dev/null +++ b/lib/simplejson/scanner.py @@ -0,0 +1,65 @@ +"""JSON token scanner +""" +import re +try: + from simplejson._speedups import make_scanner as c_make_scanner +except ImportError: + c_make_scanner = None + +__all__ = ['make_scanner'] + +NUMBER_RE = re.compile( + r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?', + (re.VERBOSE | re.MULTILINE | re.DOTALL)) + +def py_make_scanner(context): + parse_object = context.parse_object + parse_array = context.parse_array + parse_string = context.parse_string + match_number = NUMBER_RE.match + encoding = context.encoding + strict = context.strict + parse_float = context.parse_float + parse_int = context.parse_int + parse_constant = context.parse_constant + object_hook = context.object_hook + + def _scan_once(string, idx): + try: + nextchar = string[idx] + except IndexError: + raise StopIteration + + if nextchar == '"': + return parse_string(string, idx + 1, encoding, strict) + elif nextchar == '{': + return parse_object((string, idx + 1), encoding, strict, _scan_once, object_hook) + elif nextchar == '[': + return parse_array((string, idx + 1), _scan_once) + elif nextchar == 'n' and string[idx:idx + 4] == 'null': + return None, idx + 4 + elif nextchar == 't' and string[idx:idx + 4] == 'true': + return True, idx + 4 + elif nextchar == 'f' and string[idx:idx + 5] == 'false': + return False, idx + 5 + + m = match_number(string, idx) + if m is not None: + integer, frac, exp = m.groups() + if frac or exp: + res = parse_float(integer + (frac or '') + (exp or '')) + else: + res = parse_int(integer) + return res, m.end() + elif nextchar == 'N' and string[idx:idx + 3] == 'NaN': + return parse_constant('NaN'), idx + 3 + elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity': + return parse_constant('Infinity'), idx + 8 + elif nextchar == '-' and string[idx:idx + 9] == '-Infinity': + return parse_constant('-Infinity'), idx + 9 + else: + raise StopIteration + + return _scan_once + +make_scanner = c_make_scanner or py_make_scanner diff --git a/lib/simplejson/tool.py b/lib/simplejson/tool.py new file mode 100755 index 0000000..9044331 --- /dev/null +++ b/lib/simplejson/tool.py @@ -0,0 +1,37 @@ +r"""Command-line tool to validate and pretty-print JSON + +Usage:: + + $ echo '{"json":"obj"}' | python -m simplejson.tool + { + "json": "obj" + } + $ echo '{ 1.2:3.4}' | python -m simplejson.tool + Expecting property name: line 1 column 2 (char 2) + +""" +import sys +import simplejson + +def main(): + if len(sys.argv) == 1: + infile = sys.stdin + outfile = sys.stdout + elif len(sys.argv) == 2: + infile = open(sys.argv[1], 'rb') + outfile = sys.stdout + elif len(sys.argv) == 3: + infile = open(sys.argv[1], 'rb') + outfile = open(sys.argv[2], 'wb') + else: + raise SystemExit(sys.argv[0] + " [infile [outfile]]") + try: + obj = simplejson.load(infile) + except ValueError, e: + raise SystemExit(e) + simplejson.dump(obj, outfile, sort_keys=True, indent=4) + outfile.write('\n') + + +if __name__ == '__main__': + main() diff --git a/scripts/avro b/scripts/avro new file mode 100755 index 0000000..2e53afd --- /dev/null +++ b/scripts/avro @@ -0,0 +1,262 @@ +#!/usr/bin/env python + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Command line utlity for reading and writing Avro files.""" + +from avro.io import DatumReader, DatumWriter +from avro.datafile import DataFileReader, DataFileWriter +import avro.schema + +try: + import json +except ImportError: + import simplejson as json +import csv +from sys import stdout, stdin +from itertools import ifilter, imap +from functools import partial +from os.path import splitext + +class AvroError(Exception): + pass + +def print_json(row): + print(json.dumps(row)) + +def print_json_pretty(row): + print(json.dumps(row, indent=4)) + +_write_row = csv.writer(stdout).writerow +_encoding = stdout.encoding or "UTF-8" +def _encode(v, encoding=_encoding): + if not isinstance(v, basestring): + return v + return v.encode(_encoding) + +def print_csv(row): + # We sort the keys to the fields will be in the same place + # FIXME: Do we want to do it in schema order? + _write_row([_encode(row[key]) for key in sorted(row)]) + +def select_printer(format): + return { + "json" : print_json, + "json-pretty" : print_json_pretty, + "csv" : print_csv + }[format] + +def record_match(expr, record): + return eval(expr, None, {"r" : record}) + +def parse_fields(fields): + fields = fields or '' + if not fields.strip(): + return None + + return [field.strip() for field in fields.split(',') if field.strip()] + +def field_selector(fields): + fields = set(fields) + def keys_filter(obj): + return dict((k, obj[k]) for k in (set(obj) & fields)) + return keys_filter + +def print_avro(avro, opts): + if opts.header and (opts.format != "csv"): + raise AvroError("--header applies only to CSV format") + + # Apply filter first + if opts.filter: + avro = ifilter(partial(record_match, opts.filter), avro) + + for i in xrange(opts.skip): + try: + next(avro) + except StopIteration: + return + + fields = parse_fields(opts.fields) + if fields: + avro = imap(field_selector(fields), avro) + + printer = select_printer(opts.format) + for i, record in enumerate(avro): + if i == 0 and opts.header: + _write_row(sorted(record.keys())) + if i >= opts.count: + break + printer(record) + +def print_schema(avro): + schema = avro.meta["avro.schema"] + # Pretty print + print json.dumps(json.loads(schema), indent=4) + +def cat(opts, args): + if not args: + raise AvroError("No files to show") + + for filename in args: + try: + fo = open(filename, "rb") + except (OSError, IOError), e: + raise AvroError("Can't open %s - %s" % (filename, e)) + + avro = DataFileReader(fo, DatumReader()) + + if opts.print_schema: + print_schema(avro) + continue + + print_avro(avro, opts) + +def _open(filename, mode): + if filename == "-": + return { + "rb" : stdin, + "wb" : stdout + }[mode] + + return open(filename, mode) + +def iter_json(info, _): + return imap(json.loads, info) + +def convert(value, field): + type = field.type.type + if type == "union": + return convert_union(value, field) + + return { + "int" : int, + "long" : long, + "float" : float, + "double" : float, + "string" : str, + "bytes" : str, + "boolean" : bool, + "null" : lambda _: None, + "union" : lambda v: convert_union(v, field), + }[type](value) + +def convert_union(value, field): + for name in [s.name for s in field.type.schemas]: + try: + return convert(name)(value) + except ValueError: + continue + +def iter_csv(info, schema): + header = [field.name for field in schema.fields] + for row in csv.reader(info): + values = [convert(v, f) for v, f in zip(row, schema.fields)] + yield dict(zip(header, values)) + +def guess_input_type(files): + if not files: + return None + + ext = splitext(files[0])[1].lower() + if ext in (".json", ".js"): + return "json" + elif ext in (".csv",): + return "csv" + + return None + +def write(opts, files): + if not opts.schema: + raise AvroError("No schema specified") + + input_type = opts.input_type or guess_input_type(files) + if not input_type: + raise AvroError("Can't guess input file type (not .json or .csv)") + + try: + schema = avro.schema.parse(open(opts.schema, "rb").read()) + out = _open(opts.output, "wb") + except (IOError, OSError), e: + raise AvroError("Can't open file - %s" % e) + + writer = DataFileWriter(out, DatumWriter(), schema) + + iter_records = {"json" : iter_json, "csv" : iter_csv}[input_type] + for filename in (files or ["-"]): + info = _open(filename, "rb") + for record in iter_records(info, schema): + writer.append(record) + + writer.close() + +def main(argv=None): + import sys + from optparse import OptionParser, OptionGroup + + argv = argv or sys.argv + + parser = OptionParser(description="Display/write for Avro files", + version="@AVRO_VERSION@", + usage="usage: %prog cat|write [options] FILE [FILE...]") + # cat options + + cat_options = OptionGroup(parser, "cat options") + cat_options.add_option("-n", "--count", default=float("Infinity"), + help="number of records to print", type=int) + cat_options.add_option("-s", "--skip", help="number of records to skip", + type=int, default=0) + cat_options.add_option("-f", "--format", help="record format", + default="json", + choices=["json", "csv", "json-pretty"]) + cat_options.add_option("--header", help="print CSV header", default=False, + action="store_true") + cat_options.add_option("--filter", help="filter records (e.g. r['age']>1)", + default=None) + cat_options.add_option("--print-schema", help="print schema", + action="store_true", default=False) + cat_options.add_option('--fields', default=None, + help='fields to show, comma separated (show all by default)') + parser.add_option_group(cat_options) + + # write options + write_options = OptionGroup(parser, "write options") + write_options.add_option("--schema", help="schema file (required)") + write_options.add_option("--input-type", + help="input file(s) type (json or csv)", + choices=["json", "csv"], default=None) + write_options.add_option("-o", "--output", help="output file", default="-") + parser.add_option_group(write_options) + + opts, args = parser.parse_args(argv[1:]) + if len(args) < 1: + parser.error("You much specify `cat` or `write`") # Will exit + + command = args.pop(0) + try: + if command == "cat": + cat(opts, args) + elif command == "write": + write(opts, args) + else: + raise AvroError("Unknown command - %s" % command) + except AvroError, e: + parser.error("%s" % e) # Will exit + except Exception, e: + raise SystemExit("panic: %s" % e) + +if __name__ == "__main__": + main() + diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..5bf9595 --- /dev/null +++ b/setup.py @@ -0,0 +1,63 @@ +#! /usr/bin/env python + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +try: + from setuptools import setup +except ImportError: + from distutils.core import setup +from sys import version_info +from Cython.Build import cythonize + +install_requires = [] +if version_info[:2] <= (2, 5): + install_requires.append('simplejson >= 2.0.9') + +setup( + name = 'avro', + version = '@AVRO_VERSION@', + packages = ['avro',], + package_dir = {'avro': 'src/avro'}, + scripts = ["./scripts/avro"], + + #include_package_data=True, + package_data={'avro': ['LICENSE', 'NOTICE']}, + + # Project uses simplejson, so ensure that it gets installed or upgraded + # on the target machine + install_requires = install_requires, + + # fast avro code + ext_modules=cythonize("./src/avro/fast_binary.pyx"), + # metadata for upload to PyPI + author = 'Apache Avro', + author_email = 'dev@avro.apache.org', + description = 'Avro is a serialization and RPC framework.', + license = 'Apache License 2.0', + keywords = 'avro serialization rpc', + url = 'http://avro.apache.org/', + extras_require = { + 'snappy': ['python-snappy'], + }, +) + + +# from distutils.core import setup +# from Cython.Build import cythonize + +# setup( +# ext_modules=cythonize("./src/avro/fastbinary.pyx"), +# ) \ No newline at end of file diff --git a/test/av_bench.py b/test/av_bench.py new file mode 100644 index 0000000..5725997 --- /dev/null +++ b/test/av_bench.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import time +from random import sample, choice, randint +from string import lowercase + +import avro.datafile +import avro.schema +import avro.io + + +types = ["A", "CNAME"] + +def rand_name(): + return ''.join(sample(lowercase, 15)) + +def rand_ip(): + return "%s.%s.%s.%s" %(randint(0,255), randint(0,255), randint(0,255), randint(0,255)) + +def write(n): + schema_s=""" + { "type": "record", + "name": "Query", + "fields" : [ + {"name": "query", "type": "string"}, + {"name": "response", "type": "string"}, + {"name": "type", "type": "string", "default": "A"} + ]}""" + out = open("datafile.avr",'w') + + schema = avro.schema.parse(schema_s) + writer = avro.io.DatumWriter(schema) + dw = avro.datafile.DataFileWriter(out, writer, schema) #,codec='deflate') + for _ in xrange(n): + response = rand_ip() + query = rand_name() + type = choice(types) + dw.append({'query': query, 'response': response, 'type': type}) + + dw.close() + +def read(): + f = open("datafile.avr") + reader = avro.io.DatumReader() + af=avro.datafile.DataFileReader(f,reader) + + x=0 + for _ in af: + pass + +def t(f, *args): + s = time.time() + f(*args) + e = time.time() + return e-s + +if __name__ == "__main__": + n = int(sys.argv[1]) + print "Write %0.4f" % t(write, n) + print "Read %0.4f" % t(read) diff --git a/test/gen_interop_data.py b/test/gen_interop_data.py new file mode 100644 index 0000000..579505a --- /dev/null +++ b/test/gen_interop_data.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys +from avro import schema +from avro import io +from avro import datafile + +DATUM = { + 'intField': 12, + 'longField': 15234324L, + 'stringField': unicode('hey'), + 'boolField': True, + 'floatField': 1234.0, + 'doubleField': -1234.0, + 'bytesField': '12312adf', + 'nullField': None, + 'arrayField': [5.0, 0.0, 12.0], + 'mapField': {'a': {'label': 'a'}, 'bee': {'label': 'cee'}}, + 'unionField': 12.0, + 'enumField': 'C', + 'fixedField': '1019181716151413', + 'recordField': {'label': 'blah', 'children': [{'label': 'inner', 'children': []}]}, +} + +if __name__ == "__main__": + interop_schema = schema.parse(open(sys.argv[1], 'r').read()) + writer = open(sys.argv[2], 'wb') + datum_writer = io.DatumWriter() + # NB: not using compression + dfw = datafile.DataFileWriter(writer, datum_writer, interop_schema) + dfw.append(DATUM) + dfw.close() diff --git a/test/mock_tether_parent.py b/test/mock_tether_parent.py new file mode 100644 index 0000000..399a03a --- /dev/null +++ b/test/mock_tether_parent.py @@ -0,0 +1,95 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import set_avro_test_path +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +from avro import ipc +from avro import protocol +from avro import tether + +import socket + +def find_port(): + """ + Return an unbound port + """ + s=socket.socket() + s.bind(("127.0.0.1",0)) + + port=s.getsockname()[1] + s.close() + + return port + +SERVER_ADDRESS = ('localhost', find_port()) + +class MockParentResponder(ipc.Responder): + """ + The responder for the mocked parent + """ + def __init__(self): + ipc.Responder.__init__(self, tether.outputProtocol) + + def invoke(self, message, request): + if message.name=='configure': + print "MockParentResponder: Recieved 'configure': inputPort={0}".format(request["port"]) + + elif message.name=='status': + print "MockParentResponder: Recieved 'status': message={0}".format(request["message"]) + elif message.name=='fail': + print "MockParentResponder: Recieved 'fail': message={0}".format(request["message"]) + else: + print "MockParentResponder: Recieved {0}".format(message.name) + + # flush the output so it shows up in the parent process + sys.stdout.flush() + + return None + +class MockParentHandler(BaseHTTPRequestHandler): + """Create a handler for the parent. + """ + def do_POST(self): + self.responder =MockParentResponder() + call_request_reader = ipc.FramedReader(self.rfile) + call_request = call_request_reader.read_framed_message() + resp_body = self.responder.respond(call_request) + self.send_response(200) + self.send_header('Content-Type', 'avro/binary') + self.end_headers() + resp_writer = ipc.FramedWriter(self.wfile) + resp_writer.write_framed_message(resp_body) + +if __name__ == '__main__': + if (len(sys.argv)<=1): + raise ValueError("Usage: mock_tether_parent command") + + cmd=sys.argv[1].lower() + if (sys.argv[1]=='start_server'): + if (len(sys.argv)==3): + port=int(sys.argv[2]) + else: + raise ValueError("Usage: mock_tether_parent start_server port") + + SERVER_ADDRESS=(SERVER_ADDRESS[0],port) + print "mock_tether_parent: Launching Server on Port: {0}".format(SERVER_ADDRESS[1]) + + # flush the output so it shows up in the parent process + sys.stdout.flush() + parent_server = HTTPServer(SERVER_ADDRESS, MockParentHandler) + parent_server.allow_reuse_address = True + parent_server.serve_forever() diff --git a/test/sample_http_client.py b/test/sample_http_client.py new file mode 100644 index 0000000..86942d8 --- /dev/null +++ b/test/sample_http_client.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys + +from avro import ipc +from avro import protocol + +MAIL_PROTOCOL_JSON = """\ +{"namespace": "example.proto", + "protocol": "Mail", + + "types": [ + {"name": "Message", "type": "record", + "fields": [ + {"name": "to", "type": "string"}, + {"name": "from", "type": "string"}, + {"name": "body", "type": "string"} + ] + } + ], + + "messages": { + "send": { + "request": [{"name": "message", "type": "Message"}], + "response": "string" + }, + "replay": { + "request": [], + "response": "string" + } + } +} +""" +MAIL_PROTOCOL = protocol.parse(MAIL_PROTOCOL_JSON) +SERVER_HOST = 'localhost' +SERVER_PORT = 9090 + +class UsageError(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +def make_requestor(server_host, server_port, protocol): + client = ipc.HTTPTransceiver(SERVER_HOST, SERVER_PORT) + return ipc.Requestor(protocol, client) + +if __name__ == '__main__': + if len(sys.argv) not in [4, 5]: + raise UsageError("Usage: []") + + # client code - attach to the server and send a message + # fill in the Message record + message = dict() + message['to'] = sys.argv[1] + message['from'] = sys.argv[2] + message['body'] = sys.argv[3] + + try: + num_messages = int(sys.argv[4]) + except: + num_messages = 1 + + # build the parameters for the request + params = {} + params['message'] = message + + # send the requests and print the result + for msg_count in range(num_messages): + requestor = make_requestor(SERVER_HOST, SERVER_PORT, MAIL_PROTOCOL) + result = requestor.request('send', params) + print("Result: " + result) + + # try out a replay message + requestor = make_requestor(SERVER_HOST, SERVER_PORT, MAIL_PROTOCOL) + result = requestor.request('replay', dict()) + print("Replay Result: " + result) diff --git a/test/sample_http_server.py b/test/sample_http_server.py new file mode 100644 index 0000000..53f6928 --- /dev/null +++ b/test/sample_http_server.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +from avro import ipc +from avro import protocol + +MAIL_PROTOCOL_JSON = """\ +{"namespace": "example.proto", + "protocol": "Mail", + + "types": [ + {"name": "Message", "type": "record", + "fields": [ + {"name": "to", "type": "string"}, + {"name": "from", "type": "string"}, + {"name": "body", "type": "string"} + ] + } + ], + + "messages": { + "send": { + "request": [{"name": "message", "type": "Message"}], + "response": "string" + }, + "replay": { + "request": [], + "response": "string" + } + } +} +""" +MAIL_PROTOCOL = protocol.parse(MAIL_PROTOCOL_JSON) +SERVER_ADDRESS = ('localhost', 9090) + +class MailResponder(ipc.Responder): + def __init__(self): + ipc.Responder.__init__(self, MAIL_PROTOCOL) + + def invoke(self, message, request): + if message.name == 'send': + request_content = request['message'] + response = "Sent message to %(to)s from %(from)s with body %(body)s" % \ + request_content + return response + elif message.name == 'replay': + return 'replay' + +class MailHandler(BaseHTTPRequestHandler): + def do_POST(self): + self.responder = MailResponder() + call_request_reader = ipc.FramedReader(self.rfile) + call_request = call_request_reader.read_framed_message() + resp_body = self.responder.respond(call_request) + self.send_response(200) + self.send_header('Content-Type', 'avro/binary') + self.end_headers() + resp_writer = ipc.FramedWriter(self.wfile) + resp_writer.write_framed_message(resp_body) + +if __name__ == '__main__': + mail_server = HTTPServer(SERVER_ADDRESS, MailHandler) + mail_server.allow_reuse_address = True + mail_server.serve_forever() diff --git a/test/set_avro_test_path.py b/test/set_avro_test_path.py new file mode 100644 index 0000000..d8b0098 --- /dev/null +++ b/test/set_avro_test_path.py @@ -0,0 +1,40 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Module adjusts the path PYTHONPATH so the unittests +will work even if an egg for AVRO is already installed. +By default eggs always appear higher on pythons path then +directories set via the environment variable PYTHONPATH. + +For reference see: +http://www.velocityreviews.com/forums/t716589-pythonpath-and-eggs.html +http://stackoverflow.com/questions/897792/pythons-sys-path-value. + +Unittests would therefore use the installed AVRO and not the AVRO +being built. To work around this the unittests import this module before +importing AVRO. This module in turn adjusts the python path so that the test +build of AVRO is higher on the path then any installed eggs. +""" +import sys +import os + +# determine the build directory and then make sure all paths that start with the +# build directory are at the top of the path +builddir=os.path.split(os.path.split(__file__)[0])[0] +bpaths=filter(lambda s:s.startswith(builddir), sys.path) + +for p in bpaths: + sys.path.insert(0,p) \ No newline at end of file diff --git a/test/test_datafile.py b/test/test_datafile.py new file mode 100644 index 0000000..72994f3 --- /dev/null +++ b/test/test_datafile.py @@ -0,0 +1,205 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import unittest + +import set_avro_test_path + +from avro import schema +from avro import io +from avro import datafile + +SCHEMAS_TO_VALIDATE = ( + ('"null"', None), + ('"boolean"', True), + ('"string"', unicode('adsfasdf09809dsf-=adsf')), + ('"bytes"', '12345abcd'), + ('"int"', 1234), + ('"long"', 1234), + ('"float"', 1234.0), + ('"double"', 1234.0), + ('{"type": "fixed", "name": "Test", "size": 1}', 'B'), + ('{"type": "enum", "name": "Test", "symbols": ["A", "B"]}', 'B'), + ('{"type": "array", "items": "long"}', [1, 3, 2]), + ('{"type": "map", "values": "long"}', {'a': 1, 'b': 3, 'c': 2}), + ('["string", "null", "long"]', None), + ("""\ + {"type": "record", + "name": "Test", + "fields": [{"name": "f", "type": "long"}]} + """, {'f': 5}), + ("""\ + {"type": "record", + "name": "Lisp", + "fields": [{"name": "value", + "type": ["null", "string", + {"type": "record", + "name": "Cons", + "fields": [{"name": "car", "type": "Lisp"}, + {"name": "cdr", "type": "Lisp"}]}]}]} + """, {'value': {'car': {'value': 'head'}, 'cdr': {'value': None}}}), +) + +FILENAME = 'test_datafile.out' +CODECS_TO_VALIDATE = ('null', 'deflate') +try: + import snappy + CODECS_TO_VALIDATE += ('snappy',) +except ImportError: + print 'Snappy not present, will skip testing it.' + +# TODO(hammer): clean up written files with ant, not os.remove +class TestDataFile(unittest.TestCase): + def test_round_trip(self): + print '' + print 'TEST ROUND TRIP' + print '===============' + print '' + correct = 0 + for i, (example_schema, datum) in enumerate(SCHEMAS_TO_VALIDATE): + for codec in CODECS_TO_VALIDATE: + print '' + print 'SCHEMA NUMBER %d' % (i + 1) + print '================' + print '' + print 'Schema: %s' % example_schema + print 'Datum: %s' % datum + print 'Codec: %s' % codec + + # write data in binary to file 10 times + writer = open(FILENAME, 'wb') + datum_writer = io.DatumWriter() + schema_object = schema.parse(example_schema) + dfw = datafile.DataFileWriter(writer, datum_writer, schema_object, codec=codec) + for i in range(10): + dfw.append(datum) + dfw.close() + + # read data in binary from file + reader = open(FILENAME, 'rb') + datum_reader = io.DatumReader() + dfr = datafile.DataFileReader(reader, datum_reader) + round_trip_data = [] + for datum in dfr: + round_trip_data.append(datum) + + print 'Round Trip Data: %s' % round_trip_data + print 'Round Trip Data Length: %d' % len(round_trip_data) + is_correct = [datum] * 10 == round_trip_data + if is_correct: correct += 1 + print 'Correct Round Trip: %s' % is_correct + print '' + os.remove(FILENAME) + self.assertEquals(correct, len(CODECS_TO_VALIDATE)*len(SCHEMAS_TO_VALIDATE)) + + def test_append(self): + print '' + print 'TEST APPEND' + print '===========' + print '' + correct = 0 + for i, (example_schema, datum) in enumerate(SCHEMAS_TO_VALIDATE): + for codec in CODECS_TO_VALIDATE: + print '' + print 'SCHEMA NUMBER %d' % (i + 1) + print '================' + print '' + print 'Schema: %s' % example_schema + print 'Datum: %s' % datum + print 'Codec: %s' % codec + + # write data in binary to file once + writer = open(FILENAME, 'wb') + datum_writer = io.DatumWriter() + schema_object = schema.parse(example_schema) + dfw = datafile.DataFileWriter(writer, datum_writer, schema_object, codec=codec) + dfw.append(datum) + dfw.close() + + # open file, write, and close nine times + for i in range(9): + writer = open(FILENAME, 'ab+') + dfw = datafile.DataFileWriter(writer, io.DatumWriter()) + dfw.append(datum) + dfw.close() + + # read data in binary from file + reader = open(FILENAME, 'rb') + datum_reader = io.DatumReader() + dfr = datafile.DataFileReader(reader, datum_reader) + appended_data = [] + for datum in dfr: + appended_data.append(datum) + + print 'Appended Data: %s' % appended_data + print 'Appended Data Length: %d' % len(appended_data) + is_correct = [datum] * 10 == appended_data + if is_correct: correct += 1 + print 'Correct Appended: %s' % is_correct + print '' + os.remove(FILENAME) + self.assertEquals(correct, len(CODECS_TO_VALIDATE)*len(SCHEMAS_TO_VALIDATE)) + + def test_context_manager(self): + # Context manager was introduced as a first class + # member only in Python 2.6 and above. + import sys + if sys.version_info < (2,6): + print 'Skipping context manager tests on this Python version.' + return + # Test the writer with a 'with' statement. + writer = open(FILENAME, 'wb') + datum_writer = io.DatumWriter() + sample_schema, sample_datum = SCHEMAS_TO_VALIDATE[1] + schema_object = schema.parse(sample_schema) + with datafile.DataFileWriter(writer, datum_writer, schema_object) as dfw: + dfw.append(sample_datum) + self.assertTrue(writer.closed) + + # Test the reader with a 'with' statement. + datums = [] + reader = open(FILENAME, 'rb') + datum_reader = io.DatumReader() + with datafile.DataFileReader(reader, datum_reader) as dfr: + for datum in dfr: + datums.append(datum) + self.assertTrue(reader.closed) + + def test_metadata(self): + # Test the writer with a 'with' statement. + writer = open(FILENAME, 'wb') + datum_writer = io.DatumWriter() + sample_schema, sample_datum = SCHEMAS_TO_VALIDATE[1] + schema_object = schema.parse(sample_schema) + with datafile.DataFileWriter(writer, datum_writer, schema_object) as dfw: + dfw.set_meta('test.string', 'foo') + dfw.set_meta('test.number', '1') + dfw.append(sample_datum) + self.assertTrue(writer.closed) + + # Test the reader with a 'with' statement. + datums = [] + reader = open(FILENAME, 'rb') + datum_reader = io.DatumReader() + with datafile.DataFileReader(reader, datum_reader) as dfr: + self.assertEquals('foo', dfr.get_meta('test.string')) + self.assertEquals('1', dfr.get_meta('test.number')) + for datum in dfr: + datums.append(datum) + self.assertTrue(reader.closed) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_datafile_interop.py b/test/test_datafile_interop.py new file mode 100644 index 0000000..90da7d1 --- /dev/null +++ b/test/test_datafile_interop.py @@ -0,0 +1,42 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import unittest + +import set_avro_test_path + +from avro import io +from avro import datafile + +class TestDataFileInterop(unittest.TestCase): + def test_interop(self): + print '' + print 'TEST INTEROP' + print '============' + print '' + for f in os.listdir('./interop'): + print 'READING %s' % f + print '' + + # read data in binary from file + reader = open(os.path.join('./interop', f), 'rb') + datum_reader = io.DatumReader() + dfr = datafile.DataFileReader(reader, datum_reader) + for datum in dfr: + assert datum is not None + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_io.py b/test/test_io.py new file mode 100644 index 0000000..2f7135c --- /dev/null +++ b/test/test_io.py @@ -0,0 +1,341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import unittest +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO +from binascii import hexlify + +import set_avro_test_path + +from avro import schema +from avro import io + +SCHEMAS_TO_VALIDATE = ( + ('"null"', None), + ('"boolean"', True), + ('"string"', unicode('adsfasdf09809dsf-=adsf')), + ('"bytes"', '12345abcd'), + ('"int"', 1234), + ('"long"', 1234), + ('"float"', 1234.0), + ('"double"', 1234.0), + ('{"type": "fixed", "name": "Test", "size": 1}', 'B'), + ('{"type": "enum", "name": "Test", "symbols": ["A", "B"]}', 'B'), + ('{"type": "array", "items": "long"}', [1, 3, 2]), + ('{"type": "map", "values": "long"}', {'a': 1, 'b': 3, 'c': 2}), + ('["string", "null", "long"]', None), + ("""\ + {"type": "record", + "name": "Test", + "fields": [{"name": "f", "type": "long"}]} + """, {'f': 5}), + ("""\ + {"type": "record", + "name": "Lisp", + "fields": [{"name": "value", + "type": ["null", "string", + {"type": "record", + "name": "Cons", + "fields": [{"name": "car", "type": "Lisp"}, + {"name": "cdr", "type": "Lisp"}]}]}]} + """, {'value': {'car': {'value': 'head'}, 'cdr': {'value': None}}}), +) + +BINARY_ENCODINGS = ( + (0, '00'), + (-1, '01'), + (1, '02'), + (-2, '03'), + (2, '04'), + (-64, '7f'), + (64, '80 01'), + (8192, '80 80 01'), + (-8193, '81 80 01'), +) + +DEFAULT_VALUE_EXAMPLES = ( + ('"null"', 'null', None), + ('"boolean"', 'true', True), + ('"string"', '"foo"', u'foo'), + ('"bytes"', '"\u00FF\u00FF"', u'\xff\xff'), + ('"int"', '5', 5), + ('"long"', '5', 5L), + ('"float"', '1.1', 1.1), + ('"double"', '1.1', 1.1), + ('{"type": "fixed", "name": "F", "size": 2}', '"\u00FF\u00FF"', u'\xff\xff'), + ('{"type": "enum", "name": "F", "symbols": ["FOO", "BAR"]}', '"FOO"', 'FOO'), + ('{"type": "array", "items": "int"}', '[1, 2, 3]', [1, 2, 3]), + ('{"type": "map", "values": "int"}', '{"a": 1, "b": 2}', {'a': 1, 'b': 2}), + ('["int", "null"]', '5', 5), + ('{"type": "record", "name": "F", "fields": [{"name": "A", "type": "int"}]}', + '{"A": 5}', {'A': 5}), +) + +LONG_RECORD_SCHEMA = schema.parse("""\ + {"type": "record", + "name": "Test", + "fields": [{"name": "A", "type": "int"}, + {"name": "B", "type": "int"}, + {"name": "C", "type": "int"}, + {"name": "D", "type": "int"}, + {"name": "E", "type": "int"}, + {"name": "F", "type": "int"}, + {"name": "G", "type": "int"}]}""") + +LONG_RECORD_DATUM = {'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6, 'G': 7} + +def avro_hexlify(reader): + """Return the hex value, as a string, of a binary-encoded int or long.""" + bytes = [] + current_byte = reader.read(1) + bytes.append(hexlify(current_byte)) + while (ord(current_byte) & 0x80) != 0: + current_byte = reader.read(1) + bytes.append(hexlify(current_byte)) + return ' '.join(bytes) + +# @unittest.skip("skip test name") +def print_test_name(test_name): + print '' + print test_name + print '=' * len(test_name) + print '' + +def write_datum(datum, writers_schema): + writer = StringIO() + encoder = io.BinaryEncoder(writer) + datum_writer = io.DatumWriter(writers_schema) + datum_writer.write(datum, encoder) + return writer, encoder, datum_writer + +def read_datum(buffer, writers_schema, readers_schema=None): + reader = StringIO(buffer.getvalue()) + decoder = io.BinaryDecoder(reader) + datum_reader = io.DatumReader(writers_schema, readers_schema) + return datum_reader.read(decoder) + +def check_binary_encoding(number_type): + print_test_name('TEST BINARY %s ENCODING' % number_type.upper()) + correct = 0 + for datum, hex_encoding in BINARY_ENCODINGS: + print 'Datum: %d' % datum + print 'Correct Encoding: %s' % hex_encoding + + writers_schema = schema.parse('"%s"' % number_type.lower()) + writer, encoder, datum_writer = write_datum(datum, writers_schema) + writer.seek(0) + hex_val = avro_hexlify(writer) + + print 'Read Encoding: %s' % hex_val + if hex_encoding == hex_val: correct += 1 + print '' + return correct + +def check_skip_number(number_type): + print_test_name('TEST SKIP %s' % number_type.upper()) + correct = 0 + for value_to_skip, hex_encoding in BINARY_ENCODINGS: + VALUE_TO_READ = 6253 + print 'Value to Skip: %d' % value_to_skip + + # write the value to skip and a known value + writers_schema = schema.parse('"%s"' % number_type.lower()) + writer, encoder, datum_writer = write_datum(value_to_skip, writers_schema) + datum_writer.write(VALUE_TO_READ, encoder) + + # skip the value + reader = StringIO(writer.getvalue()) + decoder = io.BinaryDecoder(reader) + decoder.skip_long() + + # read data from string buffer + datum_reader = io.DatumReader(writers_schema) + read_value = datum_reader.read(decoder) + + print 'Read Value: %d' % read_value + if read_value == VALUE_TO_READ: correct += 1 + print '' + return correct + +class TestIO(unittest.TestCase): + # + # BASIC FUNCTIONALITY + # + + def test_validate(self): + print_test_name('TEST VALIDATE') + passed = 0 + for example_schema, datum in SCHEMAS_TO_VALIDATE: + print 'Schema: %s' % example_schema + print 'Datum: %s' % datum + validated = io.validate(schema.parse(example_schema), datum) + print 'Valid: %s' % validated + if validated: passed += 1 + self.assertEquals(passed, len(SCHEMAS_TO_VALIDATE)) + + def test_round_trip(self): + print_test_name('TEST ROUND TRIP') + correct = 0 + for example_schema, datum in SCHEMAS_TO_VALIDATE: + print 'Schema: %s' % example_schema + print 'Datum: %s' % datum + + writers_schema = schema.parse(example_schema) + writer, encoder, datum_writer = write_datum(datum, writers_schema) + round_trip_datum = read_datum(writer, writers_schema) + + print 'Round Trip Datum: %s' % round_trip_datum + if datum == round_trip_datum: correct += 1 + self.assertEquals(correct, len(SCHEMAS_TO_VALIDATE)) + + # + # BINARY ENCODING OF INT AND LONG + # + + def test_binary_int_encoding(self): + correct = check_binary_encoding('int') + self.assertEquals(correct, len(BINARY_ENCODINGS)) + + def test_binary_long_encoding(self): + correct = check_binary_encoding('long') + self.assertEquals(correct, len(BINARY_ENCODINGS)) + + def test_skip_int(self): + correct = check_skip_number('int') + self.assertEquals(correct, len(BINARY_ENCODINGS)) + + def test_skip_long(self): + correct = check_skip_number('long') + self.assertEquals(correct, len(BINARY_ENCODINGS)) + + # + # SCHEMA RESOLUTION + # + + def test_schema_promotion(self): + print_test_name('TEST SCHEMA PROMOTION') + # note that checking writers_schema.type in read_data + # allows us to handle promotion correctly + promotable_schemas = ['"int"', '"long"', '"float"', '"double"'] + incorrect = 0 + for i, ws in enumerate(promotable_schemas): + writers_schema = schema.parse(ws) + datum_to_write = 219 + for rs in promotable_schemas[i + 1:]: + readers_schema = schema.parse(rs) + writer, enc, dw = write_datum(datum_to_write, writers_schema) + datum_read = read_datum(writer, writers_schema, readers_schema) + print 'Writer: %s Reader: %s' % (writers_schema, readers_schema) + print 'Datum Read: %s' % datum_read + if datum_read != datum_to_write: incorrect += 1 + self.assertEquals(incorrect, 0) + + def test_unknown_symbol(self): + print_test_name('TEST UNKNOWN SYMBOL') + writers_schema = schema.parse("""\ + {"type": "enum", "name": "Test", + "symbols": ["FOO", "BAR"]}""") + datum_to_write = 'FOO' + + readers_schema = schema.parse("""\ + {"type": "enum", "name": "Test", + "symbols": ["BAR", "BAZ"]}""") + + writer, encoder, datum_writer = write_datum(datum_to_write, writers_schema) + reader = StringIO(writer.getvalue()) + decoder = io.BinaryDecoder(reader) + datum_reader = io.DatumReader(writers_schema, readers_schema) + self.assertRaises(io.SchemaResolutionException, datum_reader.read, decoder) + + def test_default_value(self): + print_test_name('TEST DEFAULT VALUE') + writers_schema = LONG_RECORD_SCHEMA + datum_to_write = LONG_RECORD_DATUM + + correct = 0 + for field_type, default_json, default_datum in DEFAULT_VALUE_EXAMPLES: + readers_schema = schema.parse("""\ + {"type": "record", "name": "Test", + "fields": [{"name": "H", "type": %s, "default": %s}]} + """ % (field_type, default_json)) + datum_to_read = {'H': default_datum} + + writer, encoder, datum_writer = write_datum(datum_to_write, writers_schema) + datum_read = read_datum(writer, writers_schema, readers_schema) + print 'Datum Read: %s' % datum_read + if datum_to_read == datum_read: correct += 1 + self.assertEquals(correct, len(DEFAULT_VALUE_EXAMPLES)) + + def test_no_default_value(self): + print_test_name('TEST NO DEFAULT VALUE') + writers_schema = LONG_RECORD_SCHEMA + datum_to_write = LONG_RECORD_DATUM + + readers_schema = schema.parse("""\ + {"type": "record", "name": "Test", + "fields": [{"name": "H", "type": "int"}]}""") + + writer, encoder, datum_writer = write_datum(datum_to_write, writers_schema) + reader = StringIO(writer.getvalue()) + decoder = io.BinaryDecoder(reader) + datum_reader = io.DatumReader(writers_schema, readers_schema) + self.assertRaises(io.SchemaResolutionException, datum_reader.read, decoder) + + def test_projection(self): + print_test_name('TEST PROJECTION') + writers_schema = LONG_RECORD_SCHEMA + datum_to_write = LONG_RECORD_DATUM + + readers_schema = schema.parse("""\ + {"type": "record", "name": "Test", + "fields": [{"name": "E", "type": "int"}, + {"name": "F", "type": "int"}]}""") + datum_to_read = {'E': 5, 'F': 6} + + writer, encoder, datum_writer = write_datum(datum_to_write, writers_schema) + datum_read = read_datum(writer, writers_schema, readers_schema) + print 'Datum Read: %s' % datum_read + self.assertEquals(datum_to_read, datum_read) + + def test_field_order(self): + print_test_name('TEST FIELD ORDER') + writers_schema = LONG_RECORD_SCHEMA + datum_to_write = LONG_RECORD_DATUM + + readers_schema = schema.parse("""\ + {"type": "record", "name": "Test", + "fields": [{"name": "F", "type": "int"}, + {"name": "E", "type": "int"}]}""") + datum_to_read = {'E': 5, 'F': 6} + + writer, encoder, datum_writer = write_datum(datum_to_write, writers_schema) + datum_read = read_datum(writer, writers_schema, readers_schema) + print 'Datum Read: %s' % datum_read + self.assertEquals(datum_to_read, datum_read) + + def test_type_exception(self): + print_test_name('TEST TYPE EXCEPTION') + writers_schema = schema.parse("""\ + {"type": "record", "name": "Test", + "fields": [{"name": "F", "type": "int"}, + {"name": "E", "type": "int"}]}""") + datum_to_write = {'E': 5, 'F': 'Bad'} + self.assertRaises(io.AvroTypeException, write_datum, datum_to_write, writers_schema) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_ipc.py b/test/test_ipc.py new file mode 100644 index 0000000..8d29c44 --- /dev/null +++ b/test/test_ipc.py @@ -0,0 +1,40 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +There are currently no IPC tests within python, in part because there are no +servers yet available. +""" +import unittest + +import set_avro_test_path + +# This test does import this code, to make sure it at least passes +# compilation. +from avro import ipc + +class TestIPC(unittest.TestCase): + def test_placeholder(self): + pass + + def test_server_with_path(self): + client_with_custom_path = ipc.HTTPTransceiver('apache.org', 80, '/service/article') + self.assertEqual('/service/article', client_with_custom_path.req_resource) + + client_with_default_path = ipc.HTTPTransceiver('apache.org', 80) + self.assertEqual('/', client_with_default_path.req_resource) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_protocol.py b/test/test_protocol.py new file mode 100644 index 0000000..8da8db1 --- /dev/null +++ b/test/test_protocol.py @@ -0,0 +1,439 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Test the protocol parsing logic. +""" +import unittest +from avro import protocol + +class ExampleProtocol(object): + def __init__(self, protocol_string, valid, name='', comment=''): + self._protocol_string = protocol_string + self._valid = valid + self._name = name or protocol_string # default to schema_string for name + self._comment = comment + + # read-only properties + protocol_string = property(lambda self: self._protocol_string) + valid = property(lambda self: self._valid) + name = property(lambda self: self._name) + + # read/write properties + def set_comment(self, new_comment): self._comment = new_comment + comment = property(lambda self: self._comment, set_comment) + +# +# Example Protocols +# +HELLO_WORLD = ExampleProtocol("""\ +{ + "namespace": "com.acme", + "protocol": "HelloWorld", + + "types": [ + {"name": "Greeting", "type": "record", "fields": [ + {"name": "message", "type": "string"}]}, + {"name": "Curse", "type": "error", "fields": [ + {"name": "message", "type": "string"}]} + ], + + "messages": { + "hello": { + "request": [{"name": "greeting", "type": "Greeting" }], + "response": "Greeting", + "errors": ["Curse"] + } + } +} + """, True) +EXAMPLES = [ + HELLO_WORLD, + ExampleProtocol("""\ +{"namespace": "org.apache.avro.test", + "protocol": "Simple", + + "types": [ + {"name": "Kind", "type": "enum", "symbols": ["FOO","BAR","BAZ"]}, + + {"name": "MD5", "type": "fixed", "size": 16}, + + {"name": "TestRecord", "type": "record", + "fields": [ + {"name": "name", "type": "string", "order": "ignore"}, + {"name": "kind", "type": "Kind", "order": "descending"}, + {"name": "hash", "type": "MD5"} + ] + }, + + {"name": "TestError", "type": "error", "fields": [ + {"name": "message", "type": "string"} + ] + } + + ], + + "messages": { + + "hello": { + "request": [{"name": "greeting", "type": "string"}], + "response": "string" + }, + + "echo": { + "request": [{"name": "record", "type": "TestRecord"}], + "response": "TestRecord" + }, + + "add": { + "request": [{"name": "arg1", "type": "int"}, {"name": "arg2", "type": "int"}], + "response": "int" + }, + + "echoBytes": { + "request": [{"name": "data", "type": "bytes"}], + "response": "bytes" + }, + + "error": { + "request": [], + "response": "null", + "errors": ["TestError"] + } + } + +} + """, True), + ExampleProtocol("""\ +{"namespace": "org.apache.avro.test.namespace", + "protocol": "TestNamespace", + + "types": [ + {"name": "org.apache.avro.test.util.MD5", "type": "fixed", "size": 16}, + {"name": "TestRecord", "type": "record", + "fields": [ {"name": "hash", "type": "org.apache.avro.test.util.MD5"} ] + }, + {"name": "TestError", "namespace": "org.apache.avro.test.errors", + "type": "error", "fields": [ {"name": "message", "type": "string"} ] + } + ], + + "messages": { + "echo": { + "request": [{"name": "record", "type": "TestRecord"}], + "response": "TestRecord" + }, + + "error": { + "request": [], + "response": "null", + "errors": ["org.apache.avro.test.errors.TestError"] + } + + } + +} + """, True), +ExampleProtocol("""\ +{"namespace": "org.apache.avro.test.namespace", + "protocol": "TestImplicitNamespace", + + "types": [ + {"name": "org.apache.avro.test.util.MD5", "type": "fixed", "size": 16}, + {"name": "ReferencedRecord", "type": "record", + "fields": [ {"name": "foo", "type": "string"} ] }, + {"name": "TestRecord", "type": "record", + "fields": [ {"name": "hash", "type": "org.apache.avro.test.util.MD5"}, + {"name": "unqalified", "type": "ReferencedRecord"} ] + }, + {"name": "TestError", + "type": "error", "fields": [ {"name": "message", "type": "string"} ] + } + ], + + "messages": { + "echo": { + "request": [{"name": "qualified", + "type": "org.apache.avro.test.namespace.TestRecord"}], + "response": "TestRecord" + }, + + "error": { + "request": [], + "response": "null", + "errors": ["org.apache.avro.test.namespace.TestError"] + } + + } + +} + """, True), +ExampleProtocol("""\ +{"namespace": "org.apache.avro.test.namespace", + "protocol": "TestNamespaceTwo", + + "types": [ + {"name": "org.apache.avro.test.util.MD5", "type": "fixed", "size": 16}, + {"name": "ReferencedRecord", "type": "record", + "namespace": "org.apache.avro.other.namespace", + "fields": [ {"name": "foo", "type": "string"} ] }, + {"name": "TestRecord", "type": "record", + "fields": [ {"name": "hash", "type": "org.apache.avro.test.util.MD5"}, + {"name": "qualified", + "type": "org.apache.avro.other.namespace.ReferencedRecord"} + ] + }, + {"name": "TestError", + "type": "error", "fields": [ {"name": "message", "type": "string"} ] + } + ], + + "messages": { + "echo": { + "request": [{"name": "qualified", + "type": "org.apache.avro.test.namespace.TestRecord"}], + "response": "TestRecord" + }, + + "error": { + "request": [], + "response": "null", + "errors": ["org.apache.avro.test.namespace.TestError"] + } + + } + +} + """, True), +ExampleProtocol("""\ +{"namespace": "org.apache.avro.test.namespace", + "protocol": "TestValidRepeatedName", + + "types": [ + {"name": "org.apache.avro.test.util.MD5", "type": "fixed", "size": 16}, + {"name": "ReferencedRecord", "type": "record", + "namespace": "org.apache.avro.other.namespace", + "fields": [ {"name": "foo", "type": "string"} ] }, + {"name": "ReferencedRecord", "type": "record", + "fields": [ {"name": "bar", "type": "double"} ] }, + {"name": "TestError", + "type": "error", "fields": [ {"name": "message", "type": "string"} ] + } + ], + + "messages": { + "echo": { + "request": [{"name": "qualified", + "type": "ReferencedRecord"}], + "response": "org.apache.avro.other.namespace.ReferencedRecord" + }, + + "error": { + "request": [], + "response": "null", + "errors": ["org.apache.avro.test.namespace.TestError"] + } + + } + +} + """, True), +ExampleProtocol("""\ +{"namespace": "org.apache.avro.test.namespace", + "protocol": "TestInvalidRepeatedName", + + "types": [ + {"name": "org.apache.avro.test.util.MD5", "type": "fixed", "size": 16}, + {"name": "ReferencedRecord", "type": "record", + "fields": [ {"name": "foo", "type": "string"} ] }, + {"name": "ReferencedRecord", "type": "record", + "fields": [ {"name": "bar", "type": "double"} ] }, + {"name": "TestError", + "type": "error", "fields": [ {"name": "message", "type": "string"} ] + } + ], + + "messages": { + "echo": { + "request": [{"name": "qualified", + "type": "ReferencedRecord"}], + "response": "org.apache.avro.other.namespace.ReferencedRecord" + }, + + "error": { + "request": [], + "response": "null", + "errors": ["org.apache.avro.test.namespace.TestError"] + } + + } + +} + """, False), + ExampleProtocol("""\ +{"namespace": "org.apache.avro.test", + "protocol": "BulkData", + + "types": [], + + "messages": { + + "read": { + "request": [], + "response": "bytes" + }, + + "write": { + "request": [ {"name": "data", "type": "bytes"} ], + "response": "null" + } + + } + +} + """, True), + ExampleProtocol("""\ +{ + "protocol" : "API", + "namespace" : "xyz.api", + "types" : [ { + "type" : "enum", + "name" : "Symbology", + "namespace" : "xyz.api.product", + "symbols" : [ "OPRA", "CUSIP", "ISIN", "SEDOL" ] + }, { + "type" : "record", + "name" : "Symbol", + "namespace" : "xyz.api.product", + "fields" : [ { + "name" : "symbology", + "type" : "xyz.api.product.Symbology" + }, { + "name" : "symbol", + "type" : "string" + } ] + }, { + "type" : "record", + "name" : "MultiSymbol", + "namespace" : "xyz.api.product", + "fields" : [ { + "name" : "symbols", + "type" : { + "type" : "map", + "values" : "xyz.api.product.Symbol" + } + } ] + } ], + "messages" : { + } +} + """, True), +] + +VALID_EXAMPLES = [e for e in EXAMPLES if e.valid] + +class TestProtocol(unittest.TestCase): + def test_parse(self): + num_correct = 0 + for example in EXAMPLES: + try: + protocol.parse(example.protocol_string) + if example.valid: + num_correct += 1 + else: + self.fail("Parsed invalid protocol: %s" % (example.name,)) + except Exception, e: + if not example.valid: + num_correct += 1 + else: + self.fail("Coudl not parse valid protocol: %s" % (example.name,)) + + fail_msg = "Parse behavior correct on %d out of %d protocols." % \ + (num_correct, len(EXAMPLES)) + self.assertEqual(num_correct, len(EXAMPLES), fail_msg) + + def test_inner_namespace_set(self): + print '' + print 'TEST INNER NAMESPACE' + print '===================' + print '' + proto = protocol.parse(HELLO_WORLD.protocol_string) + self.assertEqual(proto.namespace, "com.acme") + greeting_type = proto.types_dict['Greeting'] + self.assertEqual(greeting_type.namespace, 'com.acme') + + def test_inner_namespace_not_rendered(self): + proto = protocol.parse(HELLO_WORLD.protocol_string) + self.assertEqual('com.acme.Greeting', proto.types[0].fullname) + self.assertEqual('Greeting', proto.types[0].name) + # but there shouldn't be 'namespace' rendered to json on the inner type + self.assertFalse('namespace' in proto.to_json()['types'][0]) + + def test_valid_cast_to_string_after_parse(self): + """ + Test that the string generated by an Avro Protocol object + is, in fact, a valid Avro protocol. + """ + print '' + print 'TEST CAST TO STRING' + print '===================' + print '' + + num_correct = 0 + for example in VALID_EXAMPLES: + protocol_data = protocol.parse(example.protocol_string) + try: + try: + protocol.parse(str(protocol_data)) + debug_msg = "%s: STRING CAST SUCCESS" % example.name + num_correct += 1 + except: + debug_msg = "%s: STRING CAST FAILURE" % example.name + finally: + print debug_msg + + fail_msg = "Cast to string success on %d out of %d protocols" % \ + (num_correct, len(VALID_EXAMPLES)) + self.assertEqual(num_correct, len(VALID_EXAMPLES), fail_msg) + + def test_equivalence_after_round_trip(self): + """ + 1. Given a string, parse it to get Avro protocol "original". + 2. Serialize "original" to a string and parse that string + to generate Avro protocol "round trip". + 3. Ensure "original" and "round trip" protocols are equivalent. + """ + print '' + print 'TEST ROUND TRIP' + print '===============' + print '' + + num_correct = 0 + for example in VALID_EXAMPLES: + original_protocol = protocol.parse(example.protocol_string) + round_trip_protocol = protocol.parse(str(original_protocol)) + + if original_protocol == round_trip_protocol: + num_correct += 1 + debug_msg = "%s: ROUND TRIP SUCCESS" % example.name + else: + self.fail("Round trip failure: %s %s %s", (example.name, example.protocol_string, str(original_protocol))) + + fail_msg = "Round trip success on %d out of %d protocols" % \ + (num_correct, len(VALID_EXAMPLES)) + self.assertEqual(num_correct, len(VALID_EXAMPLES), fail_msg) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_schema.py b/test/test_schema.py new file mode 100644 index 0000000..00e2a05 --- /dev/null +++ b/test/test_schema.py @@ -0,0 +1,495 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Test the schema parsing logic. +""" +import unittest +import set_avro_test_path + +from avro import schema + +def print_test_name(test_name): + print '' + print test_name + print '=' * len(test_name) + print '' + +class ExampleSchema(object): + def __init__(self, schema_string, valid, name='', comment=''): + self._schema_string = schema_string + self._valid = valid + self._name = name or schema_string # default to schema_string for name + self.comment = comment + + @property + def schema_string(self): + return self._schema_string + + @property + def valid(self): + return self._valid + + @property + def name(self): + return self._name + +# +# Example Schemas +# + +def make_primitive_examples(): + examples = [] + for type in schema.PRIMITIVE_TYPES: + examples.append(ExampleSchema('"%s"' % type, True)) + examples.append(ExampleSchema('{"type": "%s"}' % type, True)) + return examples + +PRIMITIVE_EXAMPLES = [ + ExampleSchema('"True"', False), + ExampleSchema('True', False), + ExampleSchema('{"no_type": "test"}', False), + ExampleSchema('{"type": "panther"}', False), +] + make_primitive_examples() + +FIXED_EXAMPLES = [ + ExampleSchema('{"type": "fixed", "name": "Test", "size": 1}', True), + ExampleSchema("""\ + {"type": "fixed", + "name": "MyFixed", + "namespace": "org.apache.hadoop.avro", + "size": 1} + """, True), + ExampleSchema("""\ + {"type": "fixed", + "name": "Missing size"} + """, False), + ExampleSchema("""\ + {"type": "fixed", + "size": 314} + """, False), +] + +ENUM_EXAMPLES = [ + ExampleSchema('{"type": "enum", "name": "Test", "symbols": ["A", "B"]}', True), + ExampleSchema("""\ + {"type": "enum", + "name": "Status", + "symbols": "Normal Caution Critical"} + """, False), + ExampleSchema("""\ + {"type": "enum", + "name": [ 0, 1, 1, 2, 3, 5, 8 ], + "symbols": ["Golden", "Mean"]} + """, False), + ExampleSchema("""\ + {"type": "enum", + "symbols" : ["I", "will", "fail", "no", "name"]} + """, False), + ExampleSchema("""\ + {"type": "enum", + "name": "Test" + "symbols" : ["AA", "AA"]} + """, False), +] + +ARRAY_EXAMPLES = [ + ExampleSchema('{"type": "array", "items": "long"}', True), + ExampleSchema("""\ + {"type": "array", + "items": {"type": "enum", "name": "Test", "symbols": ["A", "B"]}} + """, True), +] + +MAP_EXAMPLES = [ + ExampleSchema('{"type": "map", "values": "long"}', True), + ExampleSchema("""\ + {"type": "map", + "values": {"type": "enum", "name": "Test", "symbols": ["A", "B"]}} + """, True), +] + +UNION_EXAMPLES = [ + ExampleSchema('["string", "null", "long"]', True), + ExampleSchema('["null", "null"]', False), + ExampleSchema('["long", "long"]', False), + ExampleSchema("""\ + [{"type": "array", "items": "long"} + {"type": "array", "items": "string"}] + """, False), +] + +RECORD_EXAMPLES = [ + ExampleSchema("""\ + {"type": "record", + "name": "Test", + "fields": [{"name": "f", + "type": "long"}]} + """, True), + ExampleSchema("""\ + {"type": "error", + "name": "Test", + "fields": [{"name": "f", + "type": "long"}]} + """, True), + ExampleSchema("""\ + {"type": "record", + "name": "Node", + "fields": [{"name": "label", "type": "string"}, + {"name": "children", + "type": {"type": "array", "items": "Node"}}]} + """, True), + ExampleSchema("""\ + {"type": "record", + "name": "Lisp", + "fields": [{"name": "value", + "type": ["null", "string", + {"type": "record", + "name": "Cons", + "fields": [{"name": "car", "type": "Lisp"}, + {"name": "cdr", "type": "Lisp"}]}]}]} + """, True), + ExampleSchema("""\ + {"type": "record", + "name": "HandshakeRequest", + "namespace": "org.apache.avro.ipc", + "fields": [{"name": "clientHash", + "type": {"type": "fixed", "name": "MD5", "size": 16}}, + {"name": "clientProtocol", "type": ["null", "string"]}, + {"name": "serverHash", "type": "MD5"}, + {"name": "meta", + "type": ["null", {"type": "map", "values": "bytes"}]}]} + """, True), + ExampleSchema("""\ + {"type": "record", + "name": "HandshakeResponse", + "namespace": "org.apache.avro.ipc", + "fields": [{"name": "match", + "type": {"type": "enum", + "name": "HandshakeMatch", + "symbols": ["BOTH", "CLIENT", "NONE"]}}, + {"name": "serverProtocol", "type": ["null", "string"]}, + {"name": "serverHash", + "type": ["null", + {"name": "MD5", "size": 16, "type": "fixed"}]}, + {"name": "meta", + "type": ["null", {"type": "map", "values": "bytes"}]}]} + """, True), + ExampleSchema("""\ + {"type": "record", + "name": "Interop", + "namespace": "org.apache.avro", + "fields": [{"name": "intField", "type": "int"}, + {"name": "longField", "type": "long"}, + {"name": "stringField", "type": "string"}, + {"name": "boolField", "type": "boolean"}, + {"name": "floatField", "type": "float"}, + {"name": "doubleField", "type": "double"}, + {"name": "bytesField", "type": "bytes"}, + {"name": "nullField", "type": "null"}, + {"name": "arrayField", + "type": {"type": "array", "items": "double"}}, + {"name": "mapField", + "type": {"type": "map", + "values": {"name": "Foo", + "type": "record", + "fields": [{"name": "label", + "type": "string"}]}}}, + {"name": "unionField", + "type": ["boolean", + "double", + {"type": "array", "items": "bytes"}]}, + {"name": "enumField", + "type": {"type": "enum", + "name": "Kind", + "symbols": ["A", "B", "C"]}}, + {"name": "fixedField", + "type": {"type": "fixed", "name": "MD5", "size": 16}}, + {"name": "recordField", + "type": {"type": "record", + "name": "Node", + "fields": [{"name": "label", "type": "string"}, + {"name": "children", + "type": {"type": "array", + "items": "Node"}}]}}]} + """, True), + ExampleSchema("""\ + {"type": "record", + "name": "ipAddr", + "fields": [{"name": "addr", + "type": [{"name": "IPv6", "type": "fixed", "size": 16}, + {"name": "IPv4", "type": "fixed", "size": 4}]}]} + """, True), + ExampleSchema("""\ + {"type": "record", + "name": "Address", + "fields": [{"type": "string"}, + {"type": "string", "name": "City"}]} + """, False), + ExampleSchema("""\ + {"type": "record", + "name": "Event", + "fields": [{"name": "Sponsor"}, + {"name": "City", "type": "string"}]} + """, False), + ExampleSchema("""\ + {"type": "record", + "fields": "His vision, from the constantly passing bars," + "name", "Rainer"} + """, False), + ExampleSchema("""\ + {"name": ["Tom", "Jerry"], + "type": "record", + "fields": [{"name": "name", "type": "string"}]} + """, False), +] + +DOC_EXAMPLES = [ + ExampleSchema("""\ + {"type": "record", + "name": "TestDoc", + "doc": "Doc string", + "fields": [{"name": "name", "type": "string", + "doc" : "Doc String"}]} + """, True), + ExampleSchema("""\ + {"type": "enum", "name": "Test", "symbols": ["A", "B"], + "doc": "Doc String"} + """, True), +] + +OTHER_PROP_EXAMPLES = [ + ExampleSchema("""\ + {"type": "record", + "name": "TestRecord", + "cp_string": "string", + "cp_int": 1, + "cp_array": [ 1, 2, 3, 4], + "fields": [ {"name": "f1", "type": "string", "cp_object": {"a":1,"b":2} }, + {"name": "f2", "type": "long", "cp_null": null} ]} + """, True), + ExampleSchema("""\ + {"type": "map", "values": "long", "cp_boolean": true} + """, True), + ExampleSchema("""\ + {"type": "enum", + "name": "TestEnum", + "symbols": [ "one", "two", "three" ], + "cp_float" : 1.0 } + """,True), + ExampleSchema("""\ + {"type": "long", + "date": "true"} + """, True) +] + +EXAMPLES = PRIMITIVE_EXAMPLES +EXAMPLES += FIXED_EXAMPLES +EXAMPLES += ENUM_EXAMPLES +EXAMPLES += ARRAY_EXAMPLES +EXAMPLES += MAP_EXAMPLES +EXAMPLES += UNION_EXAMPLES +EXAMPLES += RECORD_EXAMPLES +EXAMPLES += DOC_EXAMPLES + +VALID_EXAMPLES = [e for e in EXAMPLES if e.valid] + +# TODO(hammer): refactor into harness for examples +# TODO(hammer): pretty-print detailed output +# TODO(hammer): make verbose flag +# TODO(hammer): show strack trace to user +# TODO(hammer): use logging module? +class TestSchema(unittest.TestCase): + + def test_correct_recursive_extraction(self): + s = schema.parse('{"type": "record", "name": "X", "fields": [{"name": "y", "type": {"type": "record", "name": "Y", "fields": [{"name": "Z", "type": "X"}]}}]}') + t = schema.parse(str(s.fields[0].type)) + # If we've made it this far, the subschema was reasonably stringified; it ccould be reparsed. + self.assertEqual("X", t.fields[0].type.name) + + def test_parse(self): + correct = 0 + for example in EXAMPLES: + try: + schema.parse(example.schema_string) + if example.valid: + correct += 1 + else: + self.fail("Invalid schema was parsed: " + example.schema_string) + except: + if not example.valid: + correct += 1 + else: + self.fail("Valid schema failed to parse: " + example.schema_string) + + fail_msg = "Parse behavior correct on %d out of %d schemas." % \ + (correct, len(EXAMPLES)) + self.assertEqual(correct, len(EXAMPLES), fail_msg) + + def test_valid_cast_to_string_after_parse(self): + """ + Test that the string generated by an Avro Schema object + is, in fact, a valid Avro schema. + """ + print_test_name('TEST CAST TO STRING AFTER PARSE') + correct = 0 + for example in VALID_EXAMPLES: + schema_data = schema.parse(example.schema_string) + schema.parse(str(schema_data)) + correct += 1 + + fail_msg = "Cast to string success on %d out of %d schemas" % \ + (correct, len(VALID_EXAMPLES)) + self.assertEqual(correct, len(VALID_EXAMPLES), fail_msg) + + def test_equivalence_after_round_trip(self): + """ + 1. Given a string, parse it to get Avro schema "original". + 2. Serialize "original" to a string and parse that string + to generate Avro schema "round trip". + 3. Ensure "original" and "round trip" schemas are equivalent. + """ + print_test_name('TEST ROUND TRIP') + correct = 0 + for example in VALID_EXAMPLES: + original_schema = schema.parse(example.schema_string) + round_trip_schema = schema.parse(str(original_schema)) + if original_schema == round_trip_schema: + correct += 1 + debug_msg = "%s: ROUND TRIP SUCCESS" % example.name + else: + debug_msg = "%s: ROUND TRIP FAILURE" % example.name + self.fail("Round trip failure: %s, %s, %s" % (example.name, original_schema, str(original_schema))) + + fail_msg = "Round trip success on %d out of %d schemas" % \ + (correct, len(VALID_EXAMPLES)) + self.assertEqual(correct, len(VALID_EXAMPLES), fail_msg) + + # TODO(hammer): more tests + def test_fullname(self): + """ + The fullname is determined in one of the following ways: + * A name and namespace are both specified. For example, + one might use "name": "X", "namespace": "org.foo" + to indicate the fullname "org.foo.X". + * A fullname is specified. If the name specified contains + a dot, then it is assumed to be a fullname, and any + namespace also specified is ignored. For example, + use "name": "org.foo.X" to indicate the + fullname "org.foo.X". + * A name only is specified, i.e., a name that contains no + dots. In this case the namespace is taken from the most + tightly encosing schema or protocol. For example, + if "name": "X" is specified, and this occurs + within a field of the record definition + of "org.foo.Y", then the fullname is "org.foo.X". + + References to previously defined names are as in the latter + two cases above: if they contain a dot they are a fullname, if + they do not contain a dot, the namespace is the namespace of + the enclosing definition. + + Primitive type names have no namespace and their names may + not be defined in any namespace. A schema may only contain + multiple definitions of a fullname if the definitions are + equivalent. + """ + print_test_name('TEST FULLNAME') + + # name and namespace specified + fullname = schema.Name('a', 'o.a.h', None).fullname + self.assertEqual(fullname, 'o.a.h.a') + + # fullname and namespace specified + fullname = schema.Name('a.b.c.d', 'o.a.h', None).fullname + self.assertEqual(fullname, 'a.b.c.d') + + # name and default namespace specified + fullname = schema.Name('a', None, 'b.c.d').fullname + self.assertEqual(fullname, 'b.c.d.a') + + # fullname and default namespace specified + fullname = schema.Name('a.b.c.d', None, 'o.a.h').fullname + self.assertEqual(fullname, 'a.b.c.d') + + # fullname, namespace, default namespace specified + fullname = schema.Name('a.b.c.d', 'o.a.a', 'o.a.h').fullname + self.assertEqual(fullname, 'a.b.c.d') + + # name, namespace, default namespace specified + fullname = schema.Name('a', 'o.a.a', 'o.a.h').fullname + self.assertEqual(fullname, 'o.a.a.a') + + def test_doc_attributes(self): + print_test_name('TEST DOC ATTRIBUTES') + correct = 0 + for example in DOC_EXAMPLES: + original_schema = schema.parse(example.schema_string) + if original_schema.doc is not None: + correct += 1 + if original_schema.type == 'record': + for f in original_schema.fields: + if f.doc is None: + self.fail("Failed to preserve 'doc' in fields: " + example.schema_string) + self.assertEqual(correct,len(DOC_EXAMPLES)) + + def test_other_attributes(self): + print_test_name('TEST OTHER ATTRIBUTES') + correct = 0 + props = {} + for example in OTHER_PROP_EXAMPLES: + original_schema = schema.parse(example.schema_string) + round_trip_schema = schema.parse(str(original_schema)) + self.assertEqual(original_schema.other_props,round_trip_schema.other_props) + if original_schema.type == "record": + field_props = 0 + for f in original_schema.fields: + if f.other_props: + props.update(f.other_props) + field_props += 1 + self.assertEqual(field_props,len(original_schema.fields)) + if original_schema.other_props: + props.update(original_schema.other_props) + correct += 1 + for k in props: + v = props[k] + if k == "cp_boolean": + self.assertEqual(type(v), bool) + elif k == "cp_int": + self.assertEqual(type(v), int) + elif k == "cp_object": + self.assertEqual(type(v), dict) + elif k == "cp_float": + self.assertEqual(type(v), float) + elif k == "cp_array": + self.assertEqual(type(v), list) + self.assertEqual(correct,len(OTHER_PROP_EXAMPLES)) + + def test_exception_is_not_swallowed_on_parse_error(self): + print_test_name('TEST EXCEPTION NOT SWALLOWED ON PARSE ERROR') + + try: + schema.parse('/not/a/real/file') + caught_exception = False + except schema.SchemaParseException, e: + expected_message = 'Error parsing JSON: /not/a/real/file, error = ' \ + 'No JSON object could be decoded' + self.assertEqual(expected_message, e.args[0]) + caught_exception = True + + self.assertTrue(caught_exception, 'Exception was not caught') + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_script.py b/test/test_script.py new file mode 100644 index 0000000..d506828 --- /dev/null +++ b/test/test_script.py @@ -0,0 +1,256 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import unittest +import csv +from cStringIO import StringIO +try: + import json +except ImportError: + import simplejson as json +from tempfile import NamedTemporaryFile +import avro.schema +from avro.io import DatumWriter +from avro.datafile import DataFileWriter +from os.path import dirname, join, isfile +from os import remove +from operator import itemgetter + +NUM_RECORDS = 7 + +try: + from subprocess import check_output +except ImportError: + from subprocess import Popen, PIPE + + def check_output(args): + pipe = Popen(args, stdout=PIPE) + if pipe.wait() != 0: + raise ValueError + return pipe.stdout.read() + +try: + from subprocess import check_call +except ImportError: + def check_call(args, **kw): + pipe = Popen(args, **kw) + assert pipe.wait() == 0 + +SCHEMA = ''' +{ + "namespace": "test.avro", + "name": "LooneyTunes", + "type": "record", + "fields": [ + {"name": "first", "type": "string"}, + {"name": "last", "type": "string"}, + {"name": "type", "type": "string"} + ] +} +''' + +LOONIES = ( + ("daffy", "duck", "duck"), + ("bugs", "bunny", "bunny"), + ("tweety", "", "bird"), + ("road", "runner", "bird"), + ("wile", "e", "coyote"), + ("pepe", "le pew", "skunk"), + ("foghorn", "leghorn", "rooster"), +) + +def looney_records(): + for f, l, t in LOONIES: + yield {"first": f, "last" : l, "type" : t} + +SCRIPT = join(dirname(__file__), "..", "scripts", "avro") + +_JSON_PRETTY = '''{ + "type": "duck", + "last": "duck", + "first": "daffy" +}''' + +def gen_avro(filename): + schema = avro.schema.parse(SCHEMA) + fo = open(filename, "wb") + writer = DataFileWriter(fo, DatumWriter(), schema) + for record in looney_records(): + writer.append(record) + writer.close() + fo.close() + +def tempfile(): + return NamedTemporaryFile(delete=False).name + +class TestCat(unittest.TestCase): + def setUp(self): + self.avro_file = tempfile() + gen_avro(self.avro_file) + + def tearDown(self): + if isfile(self.avro_file): + remove(self.avro_file) + + def _run(self, *args, **kw): + out = check_output([SCRIPT, "cat", self.avro_file] + list(args)) + if kw.get("raw"): + return out + else: + return out.splitlines() + + def test_print(self): + return len(self._run()) == NUM_RECORDS + + def test_filter(self): + return len(self._run("--filter", "r['type']=='bird'")) == 2 + + def test_skip(self): + skip = 3 + return len(self._run("--skip", str(skip))) == NUM_RECORDS - skip + + def test_csv(self): + reader = csv.reader(StringIO(self._run("-f", "csv", raw=True))) + assert len(list(reader)) == NUM_RECORDS + + def test_csv_header(self): + io = StringIO(self._run("-f", "csv", "--header", raw=True)) + reader = csv.DictReader(io) + r = {"type": "duck", "last": "duck", "first": "daffy"} + assert next(reader) == r + + def test_print_schema(self): + out = self._run("--print-schema", raw=True) + assert json.loads(out)["namespace"] == "test.avro" + + def test_help(self): + # Just see we have these + self._run("-h") + self._run("--help") + + def test_json_pretty(self): + out = self._run("--format", "json-pretty", "-n", "1", raw=1) + assert out.strip() == _JSON_PRETTY.strip() + + def test_version(self): + check_output([SCRIPT, "cat", "--version"]) + + def test_files(self): + out = self._run(self.avro_file) + assert len(out) == 2 * NUM_RECORDS + + def test_fields(self): + # One field selection (no comma) + out = self._run('--fields', 'last') + assert json.loads(out[0]) == {'last': 'duck'} + + # Field selection (with comma and space) + out = self._run('--fields', 'first, last') + assert json.loads(out[0]) == {'first': 'daffy', 'last': 'duck'} + + # Empty fields should get all + out = self._run('--fields', '') + assert json.loads(out[0]) == \ + {'first': 'daffy', 'last': 'duck', 'type': 'duck'} + + # Non existing fields are ignored + out = self._run('--fields', 'first,last,age') + assert json.loads(out[0]) == {'first': 'daffy', 'last': 'duck'} + +class TestWrite(unittest.TestCase): + def setUp(self): + self.json_file = tempfile() + ".json" + fo = open(self.json_file, "w") + for record in looney_records(): + json.dump(record, fo) + fo.write("\n") + fo.close() + + self.csv_file = tempfile() + ".csv" + fo = open(self.csv_file, "w") + write = csv.writer(fo).writerow + get = itemgetter("first", "last", "type") + for record in looney_records(): + write(get(record)) + fo.close() + + self.schema_file = tempfile() + fo = open(self.schema_file, "w") + fo.write(SCHEMA) + fo.close() + + def tearDown(self): + for filename in (self.csv_file, self.json_file, self.schema_file): + try: + remove(filename) + except OSError: + continue + + def _run(self, *args, **kw): + args = [SCRIPT, "write", "--schema", self.schema_file] + list(args) + check_call(args, **kw) + + def load_avro(self, filename): + out = check_output([SCRIPT, "cat", filename]) + return map(json.loads, out.splitlines()) + + def test_version(self): + check_call([SCRIPT, "write", "--version"]) + + def format_check(self, format, filename): + tmp = tempfile() + fo = open(tmp, "wb") + self._run(filename, "-f", format, stdout=fo) + fo.close() + + records = self.load_avro(tmp) + assert len(records) == NUM_RECORDS + assert records[0]["first"] == "daffy" + + remove(tmp) + + def test_write_json(self): + self.format_check("json", self.json_file) + + def test_write_csv(self): + self.format_check("csv", self.csv_file) + + def test_outfile(self): + tmp = tempfile() + remove(tmp) + self._run(self.json_file, "-o", tmp) + + assert len(self.load_avro(tmp)) == NUM_RECORDS + remove(tmp) + + def test_multi_file(self): + tmp = tempfile() + fo = open(tmp, "wb") + self._run(self.json_file, self.json_file, stdout=fo) + fo.close() + + assert len(self.load_avro(tmp)) == 2 * NUM_RECORDS + remove(tmp) + + def test_stdin(self): + tmp = tempfile() + + info = open(self.json_file, "rb") + fo = open(tmp, "wb") + self._run("--input-type", "json", stdin=info, stdout=fo) + fo.close() + + assert len(self.load_avro(tmp)) == NUM_RECORDS + remove(tmp) diff --git a/test/test_tether_task.py b/test/test_tether_task.py new file mode 100644 index 0000000..32265e6 --- /dev/null +++ b/test/test_tether_task.py @@ -0,0 +1,116 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + + +import os +import subprocess +import sys +import time +import unittest + +import set_avro_test_path + +class TestTetherTask(unittest.TestCase): + """ + TODO: We should validate the the server response by looking at stdout + """ + def test1(self): + """ + Test that the thether_task is working. We run the mock_tether_parent in a separate + subprocess + """ + from avro import tether + from avro import io as avio + from avro import schema + from avro.tether import HTTPRequestor,inputProtocol, find_port + + import StringIO + import mock_tether_parent + from word_count_task import WordCountTask + + task=WordCountTask() + + proc=None + try: + # launch the server in a separate process + # env["AVRO_TETHER_OUTPUT_PORT"]=output_port + env=dict() + env["PYTHONPATH"]=':'.join(sys.path) + server_port=find_port() + + pyfile=mock_tether_parent.__file__ + proc=subprocess.Popen(["python", pyfile,"start_server","{0}".format(server_port)]) + input_port=find_port() + + print "Mock server started process pid={0}".format(proc.pid) + # Possible race condition? open tries to connect to the subprocess before the subprocess is fully started + # so we give the subprocess time to start up + time.sleep(1) + task.open(input_port,clientPort=server_port) + + # TODO: We should validate that open worked by grabbing the STDOUT of the subproces + # and ensuring that it outputted the correct message. + + #*************************************************************** + # Test the mapper + task.configure(tether.TaskType.MAP,str(task.inschema),str(task.midschema)) + + # Serialize some data so we can send it to the input function + datum="This is a line of text" + writer = StringIO.StringIO() + encoder = avio.BinaryEncoder(writer) + datum_writer = avio.DatumWriter(task.inschema) + datum_writer.write(datum, encoder) + + writer.seek(0) + data=writer.read() + + # Call input to simulate calling map + task.input(data,1) + + # Test the reducer + task.configure(tether.TaskType.REDUCE,str(task.midschema),str(task.outschema)) + + # Serialize some data so we can send it to the input function + datum={"key":"word","value":2} + writer = StringIO.StringIO() + encoder = avio.BinaryEncoder(writer) + datum_writer = avio.DatumWriter(task.midschema) + datum_writer.write(datum, encoder) + + writer.seek(0) + data=writer.read() + + # Call input to simulate calling reduce + task.input(data,1) + + task.complete() + + # try a status + task.status("Status message") + + except Exception as e: + raise + finally: + # close the process + if not(proc is None): + proc.kill() + + pass + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_tether_task_runner.py b/test/test_tether_task_runner.py new file mode 100644 index 0000000..a3f10fe --- /dev/null +++ b/test/test_tether_task_runner.py @@ -0,0 +1,191 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import subprocess +import sys +import time +import unittest + +import set_avro_test_path + + +class TestTetherTaskRunner(unittest.TestCase): + """ unit test for a tethered task runner. + """ + + def test1(self): + from word_count_task import WordCountTask + from avro.tether import TaskRunner, find_port,HTTPRequestor,inputProtocol, TaskType + from avro import io as avio + import mock_tether_parent + import subprocess + import StringIO + import logging + + # set the logging level to debug so that debug messages are printed + logging.basicConfig(level=logging.DEBUG) + + proc=None + try: + # launch the server in a separate process + env=dict() + env["PYTHONPATH"]=':'.join(sys.path) + parent_port=find_port() + + pyfile=mock_tether_parent.__file__ + proc=subprocess.Popen(["python", pyfile,"start_server","{0}".format(parent_port)]) + input_port=find_port() + + print "Mock server started process pid={0}".format(proc.pid) + # Possible race condition? open tries to connect to the subprocess before the subprocess is fully started + # so we give the subprocess time to start up + time.sleep(1) + + runner=TaskRunner(WordCountTask()) + + runner.start(outputport=parent_port,join=False) + + # Test sending various messages to the server and ensuring they are + # processed correctly + requestor=HTTPRequestor("localhost",runner.server.server_address[1],inputProtocol) + + # TODO: We should validate that open worked by grabbing the STDOUT of the subproces + # and ensuring that it outputted the correct message. + + # Test the mapper + requestor.request("configure",{"taskType":TaskType.MAP,"inSchema":str(runner.task.inschema),"outSchema":str(runner.task.midschema)}) + + # Serialize some data so we can send it to the input function + datum="This is a line of text" + writer = StringIO.StringIO() + encoder = avio.BinaryEncoder(writer) + datum_writer = avio.DatumWriter(runner.task.inschema) + datum_writer.write(datum, encoder) + + writer.seek(0) + data=writer.read() + + + # Call input to simulate calling map + requestor.request("input",{"data":data,"count":1}) + + #Test the reducer + requestor.request("configure",{"taskType":TaskType.REDUCE,"inSchema":str(runner.task.midschema),"outSchema":str(runner.task.outschema)}) + + #Serialize some data so we can send it to the input function + datum={"key":"word","value":2} + writer = StringIO.StringIO() + encoder = avio.BinaryEncoder(writer) + datum_writer = avio.DatumWriter(runner.task.midschema) + datum_writer.write(datum, encoder) + + writer.seek(0) + data=writer.read() + + + #Call input to simulate calling reduce + requestor.request("input",{"data":data,"count":1}) + + requestor.request("complete",{}) + + + runner.task.ready_for_shutdown.wait() + runner.server.shutdown() + #time.sleep(2) + #runner.server.shutdown() + + sthread=runner.sthread + + #Possible race condition? + time.sleep(1) + + #make sure the other thread terminated + self.assertFalse(sthread.isAlive()) + + #shutdown the logging + logging.shutdown() + + except Exception as e: + raise + finally: + #close the process + if not(proc is None): + proc.kill() + + + def test2(self): + """ + In this test we want to make sure that when we run "tether_task_runner.py" + as our main script everything works as expected. We do this by using subprocess to run it + in a separate thread. + """ + from word_count_task import WordCountTask + from avro.tether import TaskRunner, find_port,HTTPRequestor,inputProtocol, TaskType + from avro.tether import tether_task_runner + from avro import io as avio + import mock_tether_parent + import subprocess + import StringIO + + + proc=None + + runnerproc=None + try: + #launch the server in a separate process + env=dict() + env["PYTHONPATH"]=':'.join(sys.path) + parent_port=find_port() + + pyfile=mock_tether_parent.__file__ + proc=subprocess.Popen(["python", pyfile,"start_server","{0}".format(parent_port)]) + + #Possible race condition? when we start tether_task_runner it will call + # open tries to connect to the subprocess before the subprocess is fully started + #so we give the subprocess time to start up + time.sleep(1) + + + #start the tether_task_runner in a separate process + env={"AVRO_TETHER_OUTPUT_PORT":"{0}".format(parent_port)} + env["PYTHONPATH"]=':'.join(sys.path) + + runnerproc=subprocess.Popen(["python",tether_task_runner.__file__,"word_count_task.WordCountTask"],env=env) + + #possible race condition wait for the process to start + time.sleep(1) + + + + print "Mock server started process pid={0}".format(proc.pid) + #Possible race condition? open tries to connect to the subprocess before the subprocess is fully started + #so we give the subprocess time to start up + time.sleep(1) + + + except Exception as e: + raise + finally: + #close the process + if not(runnerproc is None): + runnerproc.kill() + + if not(proc is None): + proc.kill() + +if __name__==("__main__"): + unittest.main() diff --git a/test/test_tether_word_count.py b/test/test_tether_word_count.py new file mode 100644 index 0000000..6e51d31 --- /dev/null +++ b/test/test_tether_word_count.py @@ -0,0 +1,213 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import subprocess +import sys +import time +import unittest +import os + +import set_avro_test_path + +class TestTetherWordCount(unittest.TestCase): + """ unittest for a python tethered map-reduce job. + """ + + def _write_lines(self,lines,fname): + """ + Write the lines to an avro file named fname + + Parameters + -------------------------------------------------------- + lines - list of strings to write + fname - the name of the file to write to. + """ + import avro.io as avio + from avro.datafile import DataFileReader,DataFileWriter + from avro import schema + + #recursively make all directories + dparts=fname.split(os.sep)[:-1] + for i in range(len(dparts)): + pdir=os.sep+os.sep.join(dparts[:i+1]) + if not(os.path.exists(pdir)): + os.mkdir(pdir) + + + with file(fname,'w') as hf: + inschema="""{"type":"string"}""" + writer=DataFileWriter(hf,avio.DatumWriter(inschema),writers_schema=schema.parse(inschema)) + + #encoder = avio.BinaryEncoder(writer) + #datum_writer = avio.DatumWriter() + for datum in lines: + writer.append(datum) + + writer.close() + + + + + def _count_words(self,lines): + """Return a dictionary counting the words in lines + """ + counts={} + + for line in lines: + words=line.split() + + for w in words: + if not(counts.has_key(w.strip())): + counts[w.strip()]=0 + + counts[w.strip()]=counts[w.strip()]+1 + + return counts + + def test1(self): + """ + Run a tethered map-reduce job. + + Assumptions: 1) bash is available in /bin/bash + """ + from word_count_task import WordCountTask + from avro.tether import tether_task_runner + from avro.datafile import DataFileReader + from avro.io import DatumReader + import avro + + import subprocess + import StringIO + import shutil + import tempfile + import inspect + + proc=None + + try: + + + # TODO we use the tempfile module to generate random names + # for the files + base_dir = "/tmp/test_tether_word_count" + if os.path.exists(base_dir): + shutil.rmtree(base_dir) + + inpath = os.path.join(base_dir, "in") + infile=os.path.join(inpath, "lines.avro") + lines=["the quick brown fox jumps over the lazy dog", + "the cow jumps over the moon", + "the rain in spain falls mainly on the plains"] + + self._write_lines(lines,infile) + + true_counts=self._count_words(lines) + + if not(os.path.exists(infile)): + self.fail("Missing the input file {0}".format(infile)) + + + # The schema for the output of the mapper and reducer + oschema=""" +{"type":"record", + "name":"Pair","namespace":"org.apache.avro.mapred","fields":[ + {"name":"key","type":"string"}, + {"name":"value","type":"long","order":"ignore"} + ] +} +""" + + # write the schema to a temporary file + osfile=tempfile.NamedTemporaryFile(mode='w',suffix=".avsc",prefix="wordcount",delete=False) + outschema=osfile.name + osfile.write(oschema) + osfile.close() + + if not(os.path.exists(outschema)): + self.fail("Missing the schema file") + + outpath = os.path.join(base_dir, "out") + + args=[] + + args.append("java") + args.append("-jar") + args.append(os.path.abspath("@TOPDIR@/../java/tools/target/avro-tools-@AVRO_VERSION@.jar")) + + + args.append("tether") + args.extend(["--in",inpath]) + args.extend(["--out",outpath]) + args.extend(["--outschema",outschema]) + args.extend(["--protocol","http"]) + + # form the arguments for the subprocess + subargs=[] + + srcfile=inspect.getsourcefile(tether_task_runner) + + # Create a shell script to act as the program we want to execute + # We do this so we can set the python path appropriately + script="""#!/bin/bash +export PYTHONPATH={0} +python -m avro.tether.tether_task_runner word_count_task.WordCountTask +""" + # We need to make sure avro is on the path + # getsourcefile(avro) returns .../avro/__init__.py + asrc=inspect.getsourcefile(avro) + apath=asrc.rsplit(os.sep,2)[0] + + # path to where the tests lie + tpath=os.path.split(__file__)[0] + + exhf=tempfile.NamedTemporaryFile(mode='w',prefix="exec_word_count_",delete=False) + exfile=exhf.name + exhf.write(script.format((os.pathsep).join([apath,tpath]),srcfile)) + exhf.close() + + # make it world executable + os.chmod(exfile,0755) + + args.extend(["--program",exfile]) + + print "Command:\n\t{0}".format(" ".join(args)) + proc=subprocess.Popen(args) + + + proc.wait() + + # read the output + with file(os.path.join(outpath,"part-00000.avro")) as hf: + reader=DataFileReader(hf, DatumReader()) + for record in reader: + self.assertEqual(record["value"],true_counts[record["key"]]) + + reader.close() + + except Exception as e: + raise + finally: + # close the process + if proc is not None and proc.returncode is None: + proc.kill() + if os.path.exists(base_dir): + shutil.rmtree(base_dir) + if os.path.exists(exfile): + os.remove(exfile) + +if __name__== "__main__": + unittest.main() diff --git a/test/txsample_http_client.py b/test/txsample_http_client.py new file mode 100644 index 0000000..ca03c4d --- /dev/null +++ b/test/txsample_http_client.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys + +from twisted.internet import reactor, defer +from twisted.python.util import println + +from avro import protocol +from avro import txipc + +MAIL_PROTOCOL_JSON = """\ +{"namespace": "example.proto", + "protocol": "Mail", + + "types": [ + {"name": "Message", "type": "record", + "fields": [ + {"name": "to", "type": "string"}, + {"name": "from", "type": "string"}, + {"name": "body", "type": "string"} + ] + } + ], + + "messages": { + "send": { + "request": [{"name": "message", "type": "Message"}], + "response": "string" + }, + "replay": { + "request": [], + "response": "string" + } + } +} +""" +MAIL_PROTOCOL = protocol.parse(MAIL_PROTOCOL_JSON) +SERVER_HOST = 'localhost' +SERVER_PORT = 9090 + +class UsageError(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +def make_requestor(server_host, server_port, protocol): + client = txipc.TwistedHTTPTransceiver(SERVER_HOST, SERVER_PORT) + return txipc.TwistedRequestor(protocol, client) + +if __name__ == '__main__': + if len(sys.argv) not in [4, 5]: + raise UsageError("Usage: []") + + # client code - attach to the server and send a message + # fill in the Message record + message = dict() + message['to'] = sys.argv[1] + message['from'] = sys.argv[2] + message['body'] = sys.argv[3] + + try: + num_messages = int(sys.argv[4]) + except: + num_messages = 1 + + # build the parameters for the request + params = {} + params['message'] = message + + requests = [] + # send the requests and print the result + for msg_count in range(num_messages): + requestor = make_requestor(SERVER_HOST, SERVER_PORT, MAIL_PROTOCOL) + d = requestor.request('send', params) + d.addCallback(lambda result: println("Result: " + result)) + requests.append(d) + results = defer.gatherResults(requests) + + def replay_cb(result): + print("Replay Result: " + result) + reactor.stop() + + def replay(_): + # try out a replay message + requestor = make_requestor(SERVER_HOST, SERVER_PORT, MAIL_PROTOCOL) + d = requestor.request('replay', dict()) + d.addCallback(replay_cb) + + results.addCallback(replay) + reactor.run() diff --git a/test/txsample_http_server.py b/test/txsample_http_server.py new file mode 100644 index 0000000..e1d910d --- /dev/null +++ b/test/txsample_http_server.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from twisted.web import server +from twisted.internet import reactor + +from avro import ipc +from avro import protocol +from avro import txipc + +MAIL_PROTOCOL_JSON = """\ +{"namespace": "example.proto", + "protocol": "Mail", + + "types": [ + {"name": "Message", "type": "record", + "fields": [ + {"name": "to", "type": "string"}, + {"name": "from", "type": "string"}, + {"name": "body", "type": "string"} + ] + } + ], + + "messages": { + "send": { + "request": [{"name": "message", "type": "Message"}], + "response": "string" + }, + "replay": { + "request": [], + "response": "string" + } + } +} +""" +MAIL_PROTOCOL = protocol.parse(MAIL_PROTOCOL_JSON) +SERVER_ADDRESS = ('localhost', 9090) + +class MailResponder(ipc.Responder): + def __init__(self): + ipc.Responder.__init__(self, MAIL_PROTOCOL) + + def invoke(self, message, request): + if message.name == 'send': + request_content = request['message'] + response = "Sent message to %(to)s from %(from)s with body %(body)s" % \ + request_content + return response + elif message.name == 'replay': + return 'replay' + +if __name__ == '__main__': + root = server.Site(txipc.AvroResponderResource(MailResponder())) + reactor.listenTCP(9090, root) + reactor.run() diff --git a/test/word_count_task.py b/test/word_count_task.py new file mode 100644 index 0000000..30dcc51 --- /dev/null +++ b/test/word_count_task.py @@ -0,0 +1,96 @@ +""" + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +""" + +__all__=["WordCountTask"] + +from avro.tether import TetherTask + +import logging + +#TODO::Make the logging level a parameter we can set +#logging.basicConfig(level=logging.INFO) +class WordCountTask(TetherTask): + """ + Implements the mappper and reducer for the word count example + """ + + def __init__(self): + """ + """ + + inschema="""{"type":"string"}""" + midschema="""{"type":"record", "name":"Pair","namespace":"org.apache.avro.mapred","fields":[ + {"name":"key","type":"string"}, + {"name":"value","type":"long","order":"ignore"}] + }""" + outschema=midschema + TetherTask.__init__(self,inschema,midschema,outschema) + + + #keep track of the partial sums of the counts + self.psum=0 + + + def map(self,record,collector): + """Implement the mapper for the word count example + + Parameters + ---------------------------------------------------------------------------- + record - The input record + collector - The collector to collect the output + """ + + words=record.split() + + for w in words: + logging.info("WordCountTask.Map: word={0}".format(w)) + collector.collect({"key":w,"value":1}) + + def reduce(self,record, collector): + """Called with input values to generate reducer output. Inputs are sorted by the mapper + key. + + The reduce function is invoked once for each value belonging to a given key outputted + by the mapper. + + Parameters + ---------------------------------------------------------------------------- + record - The mapper output + collector - The collector to collect the output + """ + + self.psum+=record["value"] + + def reduceFlush(self,record, collector): + """ + Called with the last intermediate value in each equivalence run. + In other words, reduceFlush is invoked once for each key produced in the reduce + phase. It is called after reduce has been invoked on each value for the given key. + + Parameters + ------------------------------------------------------------------ + record - the last record on which reduce was invoked. + """ + + #collect the current record + logging.info("WordCountTask.reduceFlush key={0} value={1}".format(record["key"],self.psum)) + + collector.collect({"key":record["key"],"value":self.psum}) + + #reset the sum + self.psum=0 diff --git a/testdata/data/schema-tests.txt b/testdata/data/schema-tests.txt new file mode 100755 index 0000000..18cb6d0 --- /dev/null +++ b/testdata/data/schema-tests.txt @@ -0,0 +1,192 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +// NOTE: the Java implementation provides a slow-but-direct implementation +// of the fingerpriting algorithm which is used to cross-check the +// "fingerprint" values below. Thus, the Java unit-tests provide validation +// for these values, so other languages can just assume they are correct. + + +// 000 +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 Yefi#@w?yP9ysC;--Y{s080n~4gdfE literal 0 HcmV?d00001 diff --git a/testdata/data/test.avro12 b/testdata/data/test.avro12 new file mode 100755 index 0000000000000000000000000000000000000000..b113569a289c0cb6bfe8c0aa78002062c1cb7d1c GIT binary patch literal 3120 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 CKYDcl literal 0 HcmV?d00001 diff --git a/testdata/data/weather.avro b/testdata/data/weather.avro new file mode 100755 index 0000000000000000000000000000000000000000..b5b6b8a9f7034746b2374311f55832fc342b5666 GIT binary patch literal 358 zcmeZI%3@>@Nh~YM*GtY%NloU+E6vFf1M`cMGg5OCKQUD+l~fj_Dp@HNr6%VWr6}nr zR03n;1SE5uKtD}^XpA59sQK1~-8ik_N#1x>hdP=nn z8yYts;o|w0^y{R>-?|`%bNB@7G=EiDanEkWS_;t4O>RTxmk8h>AFSJ6QgTe|L0 iyNW1K%*f0Xrg!_I?kQ^bKn5ZS-oE;6vf3O*bc+F*-F(#m literal 0 HcmV?d00001 diff --git a/testdata/data/weather.json b/testdata/data/weather.json new file mode 100755 index 0000000..5daa227 --- /dev/null +++ b/testdata/data/weather.json @@ -0,0 +1,5 @@ +{"station":"011990-99999","time":-619524000000,"temp":0} +{"station":"011990-99999","time":-619506000000,"temp":22} +{"station":"011990-99999","time":-619484400000,"temp":-11} +{"station":"012650-99999","time":-655531200000,"temp":111} +{"station":"012650-99999","time":-655509600000,"temp":78} diff --git a/testdata/interop/bin/test_rpc_interop.sh b/testdata/interop/bin/test_rpc_interop.sh new file mode 100755 index 0000000..20ee77f --- /dev/null +++ b/testdata/interop/bin/test_rpc_interop.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e # exit on error + +cd `dirname "$0"`/../../../.. # connect to root + +VERSION=`cat share/VERSION.txt` + +#set -x # echo commands + +java_client="java -jar lang/java/tools/target/avro-tools-$VERSION.jar rpcsend" +java_server="java -jar lang/java/tools/target/avro-tools-$VERSION.jar rpcreceive" + +py_client="python lang/py/build/src/avro/tool.py rpcsend" +py_server="python lang/py/build/src/avro/tool.py rpcreceive" + +ruby_client="ruby -rubygems -Ilang/ruby/lib lang/ruby/test/tool.rb rpcsend" +ruby_server="ruby -rubygems -Ilang/ruby/lib lang/ruby/test/tool.rb rpcreceive" + +export PYTHONPATH=lang/py/build/src # path to avro Python module + +clients=("$java_client" "$py_client" "$ruby_client") +servers=("$java_server" "$py_server" "$ruby_server") + +proto=share/test/schemas/simple.avpr + +portfile=/tmp/interop_$$ + +function cleanup() { + rm -rf $portfile + for job in `jobs -p` ; do kill $job; done +} + +trap 'cleanup' EXIT + +for server in "${servers[@]}" +do + for msgDir in share/test/interop/rpc/* + do + msg=`basename "$msgDir"` + for c in ${msgDir}/* + do + echo TEST: $c + for client in "${clients[@]}" + do + rm -rf $portfile + $server http://127.0.0.1:0/ $proto $msg -file $c/response.avro \ + > $portfile & + count=0 + while [ ! -s $portfile ] + do + sleep 1 + if [ $count -ge 10 ] + then + echo $server did not start. + exit 1 + fi + count=`expr $count + 1` + done + read ignore port < $portfile + $client http://127.0.0.1:$port $proto $msg -file $c/request.avro + wait + done + done + done +done + +echo RPC INTEROP TESTS PASS diff --git a/testdata/interop/rpc/add/onePlusOne/request.avro b/testdata/interop/rpc/add/onePlusOne/request.avro new file mode 100755 index 0000000000000000000000000000000000000000..172f2374b7f10ee961f2ffdcadb7386ff08271da GIT binary patch literal 171 zcmeZI%3@>@Nh~YM*GtY%NloU+E6vFf1M`cMGg5OCk1@Nh~YM*GtY%NloU+E6vFf1M`cMGg5OCxs)>VN|YFGEL2l_s>CUJtuXFm N>CP4@CMFhiApp{w8-f4; literal 0 HcmV?d00001 diff --git a/testdata/interop/rpc/echo/foo/request.avro b/testdata/interop/rpc/echo/foo/request.avro new file mode 100755 index 0000000000000000000000000000000000000000..4d12eccc764b875a14b86f7d9c5824bab3899cf8 GIT binary patch literal 458 zcmeZI%3@>@Nh~YM*GtY%NloU+E6vFf1M`cMGg5OC@3B@Zl~fj_Dp@HNr6%VWr6}nr zWVUSDFhH zh5E}IqNcbqHz_{{@Nh~YM*GtY%NloU+E6vFf1M`cMGg5OCud!4sl~fj_Dp@HNr6%VWr6}nr z-%Fh8= zr{w1E50rLt3<8r;O0l&FJ5o}MlT-6jKo%p}laW}QfoV@#W<@HJ|9xFdfoh5~tAKts wG^?$Rt!3aAl<3qkd3Wio@y4$LWi2L5T5N9l`3wezM#d(lX66=_5Ei;J0Mved7XSbN literal 0 HcmV?d00001 diff --git a/testdata/interop/rpc/hello/world/request.avro b/testdata/interop/rpc/hello/world/request.avro new file mode 100755 index 0000000000000000000000000000000000000000..71adb631384977bf52fe0bc7e89bd466c4b0ca60 GIT binary patch literal 162 zcmeZI%3@>@Nh~YM*GtY%NloU+E6vFf1M`cMGg5OC=P_0*l~fj_Dp@HNr6%VWr6}nr z@Nh~YM*GtY%NloU+E6vFf1M`cMGg5OC1(b?QiZb)kl^6^rid*Q+u`GI} bw($Ph{B;aW5@H^yIXU?X;rT^5Dd@5QTx%eD literal 0 HcmV?d00001 diff --git a/testdata/schemas/BulkData.avpr b/testdata/schemas/BulkData.avpr new file mode 100755 index 0000000..608bf43 --- /dev/null +++ b/testdata/schemas/BulkData.avpr @@ -0,0 +1,21 @@ + +{"namespace": "org.apache.avro.test", + "protocol": "BulkData", + + "types": [], + + "messages": { + + "read": { + "request": [], + "response": "bytes" + }, + + "write": { + "request": [ {"name": "data", "type": "bytes"} ], + "response": "null" + } + + } + +} diff --git a/testdata/schemas/FooBarSpecificRecord.avsc b/testdata/schemas/FooBarSpecificRecord.avsc new file mode 100755 index 0000000..08d32b2 --- /dev/null +++ b/testdata/schemas/FooBarSpecificRecord.avsc @@ -0,0 +1,22 @@ +{ + "type": "record", + "name": "FooBarSpecificRecord", + "namespace": "org.apache.avro", + "fields": [ + {"name": "id", "type": "int"}, + {"name": "name", "type": "string"}, + {"name": "nicknames", "type": + {"type": "array", "items": "string"}}, + {"name": "relatedids", "type": + {"type": "array", "items": "int"}}, + {"name": "typeEnum", "type": + ["null", { + "type": "enum", + "name": "TypeEnum", + "namespace": "org.apache.avro", + "symbols" : ["a","b", "c"] + }], + "default": null + } + ] +} diff --git a/testdata/schemas/contexts.avdl b/testdata/schemas/contexts.avdl new file mode 100755 index 0000000..bcf9e88 --- /dev/null +++ b/testdata/schemas/contexts.avdl @@ -0,0 +1,41 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@version("1.0.5") +@namespace("org.apache.avro.ipc.specific") +protocol Contexts { + record HomePage { + } + + record ProductPage { + string product; + } + + record CartPage { + array productsInCart; + } + + record UnknownPage { + } + + record PageView { + long datetime; + union {UnknownPage, HomePage, ProductPage, CartPage} pageContext; + } + +} diff --git a/testdata/schemas/echo.avdl b/testdata/schemas/echo.avdl new file mode 100755 index 0000000..8f861ca --- /dev/null +++ b/testdata/schemas/echo.avdl @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@namespace("org.apache.avro.echo") +protocol Echo { + record Ping { + long timestamp = -1; + string text = ""; + } + + record Pong { + long timestamp = -1; + Ping ping; + } + + Pong ping(Ping ping); +} diff --git a/testdata/schemas/http.avdl b/testdata/schemas/http.avdl new file mode 100755 index 0000000..52313e7 --- /dev/null +++ b/testdata/schemas/http.avdl @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** NOTE: This structure was inspired by HTTP and deliberately skewed to get the effects that needed testing */ + +@namespace("org.apache.avro.test.http") +protocol Http { + + enum NetworkType { + IPv4, + IPv6 + } + + record NetworkConnection { + NetworkType networkType; + string networkAddress; + } + + record UserAgent { + union { null, string } id = null; + string useragent; + } + + enum HttpMethod { + GET, + POST + } + + record QueryParameter { + string name; + union { null, string } value; // Sometimes there is no value. + } + + record HttpURI { + HttpMethod method; + string path; + array parameters = []; + } + + record HttpRequest { + UserAgent userAgent; + HttpURI URI; + } + + record Request { + long timestamp; + NetworkConnection connection; + HttpRequest httpRequest; + } + +} diff --git a/testdata/schemas/interop.avsc b/testdata/schemas/interop.avsc new file mode 100755 index 0000000..8cfbba2 --- /dev/null +++ b/testdata/schemas/interop.avsc @@ -0,0 +1,28 @@ +{"type": "record", "name":"Interop", "namespace": "org.apache.avro", + "fields": [ + {"name": "intField", "type": "int"}, + {"name": "longField", "type": "long"}, + {"name": "stringField", "type": "string"}, + {"name": "boolField", "type": "boolean"}, + {"name": "floatField", "type": "float"}, + {"name": "doubleField", "type": "double"}, + {"name": "bytesField", "type": "bytes"}, + {"name": "nullField", "type": "null"}, + {"name": "arrayField", "type": {"type": "array", "items": "double"}}, + {"name": "mapField", "type": + {"type": "map", "values": + {"type": "record", "name": "Foo", + "fields": [{"name": "label", "type": "string"}]}}}, + {"name": "unionField", "type": + ["boolean", "double", {"type": "array", "items": "bytes"}]}, + {"name": "enumField", "type": + {"type": "enum", "name": "Kind", "symbols": ["A","B","C"]}}, + {"name": "fixedField", "type": + {"type": "fixed", "name": "MD5", "size": 16}}, + {"name": "recordField", "type": + {"type": "record", "name": "Node", + "fields": [ + {"name": "label", "type": "string"}, + {"name": "children", "type": {"type": "array", "items": "Node"}}]}} + ] +} diff --git a/testdata/schemas/mail.avpr b/testdata/schemas/mail.avpr new file mode 100755 index 0000000..7410592 --- /dev/null +++ b/testdata/schemas/mail.avpr @@ -0,0 +1,26 @@ +{"namespace": "org.apache.avro.test", + "protocol": "Mail", + + "types": [ + {"name": "Message", "type": "record", + "fields": [ + {"name": "to", "type": "string"}, + {"name": "from", "type": "string"}, + {"name": "body", "type": "string"} + ] + } + ], + + "messages": { + "send": { + "request": [{"name": "message", "type": "Message"}], + "response": "string" + }, + "fireandforget": { + "request": [{"name": "message", "type": "Message"}], + "response": "null", + "one-way": true + } + + } +} diff --git a/testdata/schemas/namespace.avpr b/testdata/schemas/namespace.avpr new file mode 100755 index 0000000..11b6bf1 --- /dev/null +++ b/testdata/schemas/namespace.avpr @@ -0,0 +1,28 @@ +{"namespace": "org.apache.avro.test.namespace", + "protocol": "TestNamespace", + + "types": [ + {"name": "org.apache.avro.test.util.MD5", "type": "fixed", "size": 16}, + {"name": "TestRecord", "type": "record", + "fields": [ {"name": "hash", "type": "org.apache.avro.test.util.MD5"} ] + }, + {"name": "TestError", "namespace": "org.apache.avro.test.errors", + "type": "error", "fields": [ {"name": "message", "type": "string"} ] + } + ], + + "messages": { + "echo": { + "request": [{"name": "record", "type": "TestRecord"}], + "response": "TestRecord" + }, + + "error": { + "request": [], + "response": "null", + "errors": ["org.apache.avro.test.errors.TestError"] + } + + } + +} diff --git a/testdata/schemas/nestedNullable.avdl b/testdata/schemas/nestedNullable.avdl new file mode 100755 index 0000000..a62c205 --- /dev/null +++ b/testdata/schemas/nestedNullable.avdl @@ -0,0 +1,41 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@namespace("org.apache.avro.test.nullable") +protocol Nullable { + + enum MyEnum { + One, + Two + } + + record SubRecord { + string value; + } + + record RecordWithNullables { + union { null, string } nullableString = null; + union { null, long } nullableLong = null; + union { null, int } nullableInt = null; + union { null, map } nullableMap = null; + union { null, array } nullableArray = null; + union { null, SubRecord } nullableRecord = null; + union { null, MyEnum } nullableEnum = null; + } + +} diff --git a/testdata/schemas/reserved.avsc b/testdata/schemas/reserved.avsc new file mode 100755 index 0000000..40f4849 --- /dev/null +++ b/testdata/schemas/reserved.avsc @@ -0,0 +1,2 @@ +{"name": "org.apache.avro.test.Reserved", "type": "enum", + "symbols": ["default","class","int"]}, diff --git a/testdata/schemas/schemaevolution.avdl b/testdata/schemas/schemaevolution.avdl new file mode 100755 index 0000000..bfb14c5 --- /dev/null +++ b/testdata/schemas/schemaevolution.avdl @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A few simple test schemas for testing schema evolution the IDL generated classes + */ +@namespace("org.apache.avro.compiler.schema.evolve") +protocol SchemaEvolveTesting { + record TestRecord1 { + string name; + long value; + } + + record TestRecord2 { + string name; + long value; + string data; + } + + record TestRecord3 { + string name; + string data; + } + + record NestedEvolve1 { + string rootName; + TestRecord1 nested; + } + + record NestedEvolve2 { + string rootName; + TestRecord2 nested; + } + + record NestedEvolve3 { + string rootName; + TestRecord3 nested; + } + +} diff --git a/testdata/schemas/simple.avpr b/testdata/schemas/simple.avpr new file mode 100755 index 0000000..c1f2a4c --- /dev/null +++ b/testdata/schemas/simple.avpr @@ -0,0 +1,80 @@ +{"namespace": "org.apache.avro.test", + "protocol": "Simple", + "doc": "Protocol used for testing.", + "version" : "1.6.2", + "javaAnnotation": ["javax.annotation.Generated(\"avro\")", + "org.apache.avro.TestAnnotation"], + + "types": [ + {"name": "Kind", "type": "enum", "symbols": ["FOO","BAR","BAZ"], + "javaAnnotation": "org.apache.avro.TestAnnotation"}, + + {"name": "MD5", "type": "fixed", "size": 16, + "javaAnnotation": "org.apache.avro.TestAnnotation"}, + + {"name": "TestRecord", "type": "record", + "javaAnnotation": "org.apache.avro.TestAnnotation", + "fields": [ + {"name": "name", "type": "string", "order": "ignore", + "javaAnnotation": "org.apache.avro.TestAnnotation"}, + {"name": "kind", "type": "Kind", "order": "descending"}, + {"name": "hash", "type": "MD5"} + ] + }, + + {"name": "TestError", "type": "error", "fields": [ + {"name": "message", "type": "string"} + ] + }, + + {"name": "TestRecordWithUnion", "type": "record", + "fields": [ + {"name": "kind", "type": ["null", "Kind"]}, + {"name": "value", "type": ["null", "string"]} + ] + } + + ], + + "messages": { + + "hello": { + "doc": "Send a greeting", + "request": [{"name": "greeting", "type": "string", "aliases" : [ "salute" ], "customProp" : "customValue"}], + "response": "string" + }, + + "echo": { + "doc": "Pretend you're in a cave!", + "request": [{"name": "record", "type": "TestRecord"}], + "response": "TestRecord" + }, + + "add": { + "specialProp" : "test", + "request": [{"name": "arg1", "type": "int"}, {"name": "arg2", "type": "int"}], + "response": "int" + }, + + "echoBytes": { + "request": [{"name": "data", "type": "bytes"}], + "response": "bytes" + }, + + "error": { + "doc": "Always throws an error.", + "request": [], + "response": "null", + "errors": ["TestError"] + }, + + "ack": { + "doc": "Send a one way message", + "request": [], + "response": "null", + "one-way": true, + "javaAnnotation": "org.apache.avro.TestAnnotation" + } + } + +} diff --git a/testdata/schemas/social.avdl b/testdata/schemas/social.avdl new file mode 100755 index 0000000..3212418 --- /dev/null +++ b/testdata/schemas/social.avdl @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@version("1.0.5") +@namespace("org.apache.avro.ipc.specific") +protocol Social { + enum PrivacyType { FRIENDS, FRIENDS_OF_FRIENDS, PUBLIC, CUSTOM } + + record Person { + string name; + int year_of_birth; + string country = "US"; + string state; + array friends = []; + array languages = [ "English" , "Java" ]; + PrivacyType defaultPrivacy = "FRIENDS"; + } +} diff --git a/testdata/schemas/specialtypes.avdl b/testdata/schemas/specialtypes.avdl new file mode 100755 index 0000000..062398f --- /dev/null +++ b/testdata/schemas/specialtypes.avdl @@ -0,0 +1,109 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** NOTE: This structure is intended to contain names that are likely to cause collisions with the generated code. */ + +@namespace("org.apache.avro.test.specialtypes") +protocol LetsBreakIt { + + enum Enum { + builder, + Builder, + builderBuider, + value, + this + } + + record One { + Enum this; + } + + record Two { + union { null, string } this = null; + string String; + } + + record Variables { + One this; + + One Boolean; + One Integer; + One Long; + One Float; + One String; + } + + enum Boolean { + Yes, + No + } + + record String { + string value; + } + + record builder { + One this; + Two builder; + } + + record builderBuilder { + One this; + Two that; + } + + record Builder { + One this; + Two that; + } + + record value { + One this; + Two that; + } + + record Types { + Boolean one; + builder two; + Builder three; + builderBuilder four; + String five; + value six; + } + + record Names { + string Boolean; + string builder; + string Builder; + string builderBuilder; + string String; + string value; + } + + record TopLevelDomainNames { + string org; + string avro; + string com; + string net; + string nl; + } + + record Exception { + string whatever; + } +} diff --git a/testdata/schemas/stringables.avdl b/testdata/schemas/stringables.avdl new file mode 100755 index 0000000..ce6173e --- /dev/null +++ b/testdata/schemas/stringables.avdl @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A test case to exercise the stringable feature on @java-class, @java-key-class and + * @java-element-class + */ +@namespace("test") +protocol AnnotatedStringableTypes { + + record StringablesRecord { + /** Each field exercises one of the java-class, key-class or element-class. */ + @java-class("java.math.BigDecimal") string value; + @java-key-class("java.math.BigInteger") map mapWithBigIntKeys; + map<@java-class("java.math.BigDecimal") string> mapWithBigDecimalElements; + } +} diff --git a/testdata/schemas/weather.avsc b/testdata/schemas/weather.avsc new file mode 100755 index 0000000..db3a43f --- /dev/null +++ b/testdata/schemas/weather.avsc @@ -0,0 +1,8 @@ +{"type": "record", "name": "test.Weather", + "doc": "A weather reading.", + "fields": [ + {"name": "station", "type": "string", "order": "ignore"}, + {"name": "time", "type": "long"}, + {"name": "temp", "type": "int"} + ] +}