# Copyright 2015 Google Inc. All rights reserved.
#
# 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.
import io
import json
import math
import os
import sys
import unittest
from collections import OrderedDict
from string import printable
import json5
try:
# Make the `hypothesis` library optional, so that the other tests will
# still run if it isn't installed.
import hypothesis.strategies as some
from hypothesis import given
some_json = some.recursive(
some.none() |
some.booleans() |
some.floats(allow_nan=False) |
some.text(printable),
lambda children: some.lists(children, min_size=1)
| some.dictionaries(some.text(printable), children, min_size=1),
)
except ImportError as e:
def given(x):
def func(y):
pass
return func
some_json = {}
class TestLoads(unittest.TestCase):
maxDiff = None
def check(self, s, obj):
self.assertEqual(json5.loads(s), obj)
def check_fail(self, s, err=None):
try:
json5.loads(s)
self.fail() # pragma: no cover
except ValueError as e:
if err:
self.assertEqual(err, str(e))
def test_arrays(self):
self.check('[]', [])
self.check('[0]', [0])
self.check('[0,1]', [0, 1])
self.check('[ 0 , 1 ]', [0, 1])
try:
json5.loads('[ ,]')
self.fail()
except ValueError as e:
self.assertIn('Unexpected "," at column 3', str(e))
def test_bools(self):
self.check('true', True)
self.check('false', False)
def test_cls_is_not_supported(self):
self.assertRaises(AssertionError, json5.loads, '1', cls=lambda x: x)
def test_duplicate_keys_should_be_allowed(self):
self.assertEqual(json5.loads('{foo: 1, foo: 2}',
allow_duplicate_keys=True),
{"foo": 2})
def test_duplicate_keys_should_be_allowed_by_default(self):
self.check('{foo: 1, foo: 2}', {"foo": 2})
def test_duplicate_keys_should_not_be_allowed(self):
self.assertRaises(ValueError, json5.loads, '{foo: 1, foo: 2}',
allow_duplicate_keys=False)
def test_empty_strings_are_errors(self):
self.check_fail('', 'Empty strings are not legal JSON5')
def test_encoding(self):
if sys.version_info[0] < 3:
s = '"\xf6"'
else:
s = b'"\xf6"'
self.assertEqual(json5.loads(s, encoding='iso-8859-1'),
u'\xf6')
def test_numbers(self):
# decimal literals
self.check('1', 1)
self.check('-1', -1)
self.check('+1', 1)
# hex literals
self.check('0xf', 15)
self.check('0xfe', 254)
self.check('0xfff', 4095)
self.check('0XABCD', 43981)
self.check('0x123456', 1193046)
# floats
self.check('1.5', 1.5)
self.check('1.5e3', 1500.0)
self.check('-0.5e-2', -0.005)
# names
self.check('Infinity', float('inf'))
self.check('-Infinity', float('-inf'))
self.assertTrue(math.isnan(json5.loads('NaN')))
self.assertTrue(math.isnan(json5.loads('-NaN')))
# syntax errors
self.check_fail('14d', '<string>:1 Unexpected "d" at column 3')
def test_identifiers(self):
self.check('{a: 1}', {'a': 1})
self.check('{$: 1}', {'$': 1})
self.check('{_: 1}', {'_': 1})
self.check('{a_b: 1}', {'a_b': 1})
self.check('{a$: 1}', {'a$': 1})
# This valid JavaScript but not valid JSON5; keys must be identifiers
# or strings.
self.check_fail('{1: 1}')
def test_identifiers_unicode(self):
self.check(u'{\xc3: 1}', {u'\xc3': 1})
def test_null(self):
self.check('null', None)
def test_object_hook(self):
hook = lambda d: [d]
self.assertEqual(json5.loads('{foo: 1}', object_hook=hook),
[{"foo": 1}])
def test_object_pairs_hook(self):
hook = lambda pairs: pairs
self.assertEqual(json5.loads('{foo: 1, bar: 2}',
object_pairs_hook=hook),
[('foo', 1), ('bar', 2)])
def test_objects(self):
self.check('{}', {})
self.check('{"foo": 0}', {"foo": 0})
self.check('{"foo":0,"bar":1}', {"foo": 0, "bar": 1})
self.check('{ "foo" : 0 , "bar" : 1 }', {"foo": 0, "bar": 1})
def test_parse_constant(self):
hook = lambda x: x
self.assertEqual(json5.loads('-Infinity', parse_constant=hook),
'-Infinity')
self.assertEqual(json5.loads('NaN', parse_constant=hook),
'NaN')
def test_parse_float(self):
hook = lambda x: x
self.assertEqual(json5.loads('1.0', parse_float=hook), '1.0')
def test_parse_int(self):
hook = lambda x, base=10: x
self.assertEqual(json5.loads('1', parse_int=hook), '1')
def test_sample_file(self):
path = os.path.join(os.path.dirname(__file__), '..', 'sample.json5')
with open(path) as fp:
obj = json5.load(fp)
self.assertEqual({
u'oh': [
u"we shouldn't forget",
u"arrays can have",
u"trailing commas too",
],
u"this": u"is a multi-line string",
u"delta": 10,
u"hex": 3735928559,
u"finally": "a trailing comma",
u"here": "is another",
u"to": float("inf"),
u"while": True,
u"half": 0.5,
u"foo": u"bar"
}, obj)
def test_strings(self):
self.check('"foo"', 'foo')
self.check("'foo'", 'foo')
# escape chars
self.check("'\\b\\t\\f\\n\\r\\v\\\\'", '\b\t\f\n\r\v\\')
self.check("'\\''", "'")
self.check('"\\""', '"')
# hex literals
self.check('"\\x66oo"', 'foo')
# unicode literals
self.check('"\\u0066oo"', 'foo')
# string literals w/ continuation markers at the end of the line.
# These should not have spaces is the result.
self.check('"foo\\\nbar"', 'foobar')
self.check("'foo\\\nbar'", 'foobar')
# unterminated string literals.
self.check_fail('"\n')
self.check_fail("'\n")
# bad hex literals
self.check_fail("'\\x0'")
self.check_fail("'\\xj'")
self.check_fail("'\\x0j'")
# bad unicode literals
self.check_fail("'\\u0'")
self.check_fail("'\\u00'")
self.check_fail("'\\u000'")
self.check_fail("'\\u000j'")
self.check_fail("'\\u00j0'")
self.check_fail("'\\u0j00'")
self.check_fail("'\\uj000'")
def test_unrecognized_escape_char(self):
self.check(r'"\/"', '/')
def test_nul(self):
self.check(r'"\0"', '\x00')
def test_whitespace(self):
self.check('\n1', 1)
self.check('\r1', 1)
self.check('\r\n1', 1)
self.check('\t1', 1)
self.check('\v1', 1)
self.check(u'\uFEFF 1', 1)
self.check(u'\u00A0 1', 1)
self.check(u'\u2028 1', 1)
self.check(u'\u2029 1', 1)
def test_error_reporting(self):
self.check_fail('[ ,]',
err='<string>:1 Unexpected "," at column 3')
self.check_fail(
'{\n'
' version: "1.0",\n'
' author: "John Smith",\n'
' people : [\n'
' "Monty",\n'
' "Python"foo\n'
' ]\n'
'}\n',
err='<string>:6 Unexpected "f" at column 17')
class TestDump(unittest.TestCase):
def test_basic(self):
sio = io.StringIO()
json5.dump(True, sio)
self.assertEqual('true', sio.getvalue())
class TestDumps(unittest.TestCase):
maxDiff = None
def check(self, obj, s):
self.assertEqual(json5.dumps(obj), s)
def test_allow_duplicate_keys(self):
self.assertIn(json5.dumps({1: "foo", "1": "bar"}),
{'{"1": "foo", "1": "bar"}',
'{"1": "bar", "1": "foo"}'})
self.assertRaises(ValueError, json5.dumps,
{1: "foo", "1": "bar"},
allow_duplicate_keys=False)
def test_arrays(self):
self.check([], '[]')
self.check([1, 2, 3], '[1, 2, 3]')
self.check([{'foo': 'bar'}, {'baz': 'quux'}],
'[{foo: "bar"}, {baz: "quux"}]')
def test_bools(self):
self.check(True, 'true')
self.check(False, 'false')
def test_check_circular(self):
# This tests a trivial cycle.
l = [1, 2, 3]
l[2] = l
self.assertRaises(ValueError, json5.dumps, l)
# This checks that json5 doesn't raise an error. However,
# the underlying Python implementation likely will.
try:
json5.dumps(l, check_circular=False)
self.fail() # pragma: no cover
except Exception as e:
self.assertNotIn(str(e), 'Circular reference detected')
# This checks that repeated but non-circular references
# are okay.
x = [1, 2]
y = {"foo": x, "bar": x}
self.check(y,
'{foo: [1, 2], bar: [1, 2]}')
# This tests a more complicated cycle.
x = {}
y = {}
z = {}
z['x'] = x
z['y'] = y
z['x']['y'] = y
z['y']['x'] = x
self.assertRaises(ValueError, json5.dumps, z)
def test_custom_arrays(self):
class MyArray(object):
def __iter__(self):
yield 0
yield 1
yield 1
def __getitem__(self, i):
return 0 if i == 0 else 1
def __len__(self):
return 3
self.assertEqual(json5.dumps(MyArray()), '[0, 1, 1]')
def test_custom_numbers(self):
# See https://github.com/dpranke/pyjson5/issues/57: we
# need to ensure that we use the bare int.__repr__ and
# float.__repr__ in order to get legal JSON values when
# people have custom subclasses with customer __repr__ methods.
# (This is what JSON does and we want to match it).
class MyInt(int):
def __repr__(self):
return 'fail'
self.assertEqual(json5.dumps(MyInt(5)), '5')
class MyFloat(float):
def __repr__(self):
return 'fail'
self.assertEqual(json5.dumps(MyFloat(0.5)), '0.5')
def test_custom_objects(self):
class MyDict(object):
def __iter__(self):
yield ('a', 1)
yield ('b', 2)
def keys(self):
return ['a', 'b']
def __getitem__(self, k):
return {'a': 1, 'b': 2}[k]
def __len__(self):
return 2
self.assertEqual(json5.dumps(MyDict()), '{a: 1, b: 2}')
def test_custom_strings(self):
class MyStr(str):
pass
self.assertEqual(json5.dumps({'foo': MyStr('bar')}), '{foo: "bar"}')
def test_default(self):
def _custom_serializer(obj):
del obj
return 'something'
self.assertRaises(TypeError, json5.dumps, set())
self.assertEqual(json5.dumps(set(), default=_custom_serializer),
'"something"')
def test_ensure_ascii(self):
self.check(u'\u00fc', '"\\u00fc"')
self.assertEqual(json5.dumps(u'\u00fc', ensure_ascii=False),
u'"\u00fc"')
def test_indent(self):
self.assertEqual(json5.dumps([1, 2, 3], indent=None),
u'[1, 2, 3]')
self.assertEqual(json5.dumps([1, 2, 3], indent=-1),
u'[\n1,\n2,\n3,\n]')
self.assertEqual(json5.dumps([1, 2, 3], indent=0),
u'[\n1,\n2,\n3,\n]')
self.assertEqual(json5.dumps([], indent=2),
u'[]')
self.assertEqual(json5.dumps([1, 2, 3], indent=2),
u'[\n 1,\n 2,\n 3,\n]')
self.assertEqual(json5.dumps([1, 2, 3], indent=' '),
u'[\n 1,\n 2,\n 3,\n]')
self.assertEqual(json5.dumps([1, 2, 3], indent='++'),
u'[\n++1,\n++2,\n++3,\n]')
self.assertEqual(json5.dumps([[1, 2, 3]], indent=2),
u'[\n [\n 1,\n 2,\n 3,\n ],\n]')
self.assertEqual(json5.dumps({}, indent=2),
u'{}')
self.assertEqual(json5.dumps({'foo': 'bar', 'baz': 'quux'}, indent=2),
u'{\n foo: "bar",\n baz: "quux",\n}')
def test_numbers(self):
self.check(15, '15')
self.check(1.0, '1.0')
self.check(float('inf'), 'Infinity')
self.check(float('-inf'), '-Infinity')
self.check(float('nan'), 'NaN')
self.assertRaises(ValueError, json5.dumps,
float('inf'), allow_nan=False)
self.assertRaises(ValueError, json5.dumps,
float('-inf'), allow_nan=False)
self.assertRaises(ValueError, json5.dumps,
float('nan'), allow_nan=False)
def test_null(self):
self.check(None, 'null')
def test_objects(self):
self.check({'foo': 1}, '{foo: 1}')
self.check({'foo bar': 1}, '{"foo bar": 1}')
self.check({'1': 1}, '{"1": 1}')
def test_reserved_words_in_object_keys_are_quoted(self):
self.check({'new': 1}, '{"new": 1}')
def test_identifiers_only_starting_with_reserved_words_are_not_quoted(self):
self.check({'newbie': 1}, '{newbie: 1}')
def test_non_string_keys(self):
self.assertEqual(json5.dumps({False: 'a', 1: 'b', 2.0: 'c', None: 'd'}),
'{"false": "a", "1": "b", "2.0": "c", "null": "d"}')
def test_quote_keys(self):
self.assertEqual(json5.dumps({"foo": 1}, quote_keys=True),
'{"foo": 1}')
def test_strings(self):
self.check("'single'", '"\'single\'"')
self.check('"double"', '"\\"double\\""')
self.check("'single \\' and double \"'",
'"\'single \\\\\' and double \\"\'"')
def test_string_escape_sequences(self):
self.check(u'\u2028\u2029\b\t\f\n\r\v\\\0',
'"\\u2028\\u2029\\b\\t\\f\\n\\r\\v\\\\\\0"')
def test_skip_keys(self):
od = OrderedDict()
od[(1, 2)] = 2
self.assertRaises(TypeError, json5.dumps, od)
self.assertEqual(json5.dumps(od, skipkeys=True), '{}')
od['foo'] = 1
self.assertEqual(json5.dumps(od, skipkeys=True), '{foo: 1}')
# Also test that having an invalid key as the last element
# doesn't incorrectly add a trailing comma (see
# https://github.com/dpranke/pyjson5/issues/33).
od = OrderedDict()
od['foo'] = 1
od[(1, 2)] = 2
self.assertEqual(json5.dumps(od, skipkeys=True), '{foo: 1}')
def test_sort_keys(self):
od = OrderedDict()
od['foo'] = 1
od['bar'] = 2
self.assertEqual(json5.dumps(od, sort_keys=True),
'{bar: 2, foo: 1}')
def test_trailing_commas(self):
# By default, multi-line dicts and lists should have trailing
# commas after their last items.
self.assertEqual(json5.dumps({"foo": 1}, indent=2),
'{\n foo: 1,\n}')
self.assertEqual(json5.dumps([1], indent=2),
'[\n 1,\n]')
self.assertEqual(json5.dumps({"foo": 1}, indent=2,
trailing_commas=False),
'{\n foo: 1\n}')
self.assertEqual(json5.dumps([1], indent=2, trailing_commas=False),
'[\n 1\n]')
def test_supplemental_unicode(self):
try:
s = chr(0x10000)
self.check(s, '"\\ud800\\udc00"')
except ValueError:
# Python2 doesn't support supplemental unicode planes, so
# we can't test this there.
pass
def test_empty_key(self):
self.assertEqual(json5.dumps({'': 'value'}), '{"": "value"}')
@given(some_json)
def test_object_roundtrip(self, input_object):
dumped_string_json = json.dumps(input_object)
dumped_string_json5 = json5.dumps(input_object)
parsed_object_json = json5.loads(dumped_string_json)
parsed_object_json5 = json5.loads(dumped_string_json5)
assert parsed_object_json == input_object
assert parsed_object_json5 == input_object
if __name__ == '__main__': # pragma: no cover
unittest.main()