blob: 75ba7c7c47b8afa3779a10292f133e186970e8fb [file] [log] [blame]
# 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 math
import os
import unittest
from collections import OrderedDict
import json5
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 is not None:
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() # pragma: no cover
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,
)
# Also check to make sure we don't reject things incorrectly.
self.assertEqual(
json5.loads('{foo: 1, bar: 2}', allow_duplicate_keys=False),
{'foo': 1, 'bar': 2},
)
def test_empty_strings_are_errors(self):
self.check_fail('', 'Empty strings are not legal JSON5')
def test_partial_strings_are_errors(self):
self.check_fail("'", '<string>:1 Unexpected end of input at column 2')
def test_encoding(self):
self.assertEqual(json5.loads(b'"\xf6"', encoding='iso-8859-1'), '\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)
self.check_fail('0x+', '<string>:1 Unexpected "+" at column 3')
# 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.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):
# It would be silly to try and test all of the possible unicode
# characters for correctness, but we can at least check each
# legal Unicode category.
# Latin Capital letter A with Tilde, category Lu (uppercase letter)
self.check('{\xc3: 1}', {'\xc3': 1})
# Latin small A with Ring above, category Ll (lowercase letter)
self.check('{\u00e5: 1}', {'\u00e5': 1})
# Modifier Letter small H, category Lm (modifier letter)
self.check('{\u02b0: 1}', {'\u02b0': 1})
# Latin Letter Two with Stroke, category Lo (other letter)
self.check('{\u01bb: 1}', {'\u01bb': 1})
# Latin Capital Letter L with Small Letter J
# (category Lt, titlecase letter)
self.check('{\u01c8: 1}', {'\u01c8': 1})
# Roman Numeral One (category Nl, letter number)
self.check('{\u2160: 1}', {'\u2160': 1})
# Combining Diaresis (category Mn, non-spacing mark)
self.check('{a\u0308o: 1}', {'a\u0308o': 1})
# Rejang Virama (category Mc, spacing mark)
self.check('{a\ua953o: 1}', {'a\ua953o': 1})
# Arabic-Indic Digit Zero (category Nd, decimal number)
self.check('{a\u0660: 1}', {'a\u0660': 1})
# Undertie (category Pc, connector punctuation)
self.check('{a\u203fb: 1}', {'a\u203fb': 1})
def test_null(self):
self.check('null', None)
def test_object_hook(self):
def hook(d):
return [d]
self.assertEqual(
json5.loads('{foo: 1}', object_hook=hook), [{'foo': 1}]
)
def test_object_pairs_hook(self):
def hook(pairs):
return 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):
def hook(x):
return x
self.assertEqual(
json5.loads('-Infinity', parse_constant=hook), '-Infinity'
)
self.assertEqual(json5.loads('NaN', parse_constant=hook), 'NaN')
def test_parse_float(self):
def hook(x):
return x
self.assertEqual(json5.loads('1.0', parse_float=hook), '1.0')
def test_parse_int(self):
def hook(x, base=10):
del base
return 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, encoding='utf-8') as fp:
obj = json5.load(fp)
self.assertEqual(
{
'oh': [
"we shouldn't forget",
'arrays can have',
'trailing commas too',
],
'this': 'is a multi-line string',
'delta': 10,
'hex': 3735928559,
'finally': 'a trailing comma',
'here': 'is another',
'to': float('inf'),
'while': True,
'half': 0.5,
'foo': '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):
# Whitespace should be allowed before and after a value.
self.check('\n1', 1)
self.check('\r1', 1)
self.check('\r\n1', 1)
self.check('\t1', 1)
self.check('\v1', 1)
self.check('\ufeff 1', 1)
self.check('\u00a0 1', 1)
self.check('\u2028 1', 1) # line separator
self.check('\u2029 1', 1) # paragraph separator
self.check('\u2000 1', 1) # En quad, unicode category Zs
self.check('1\n', 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',
)
def test_no_extra_characters_in_value(self):
self.check_fail('0 1', '<string>:1 Unexpected "1" at column 3')
self.check_fail('0 a', '<string>:1 Unexpected "a" at column 3')
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.
obj = [1, 2, 3]
obj[2] = obj
self.assertRaises(ValueError, json5.dumps, obj)
# This checks that json5 doesn't raise an error. However,
# the underlying Python implementation likely will.
try:
json5.dumps(obj, 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):
# A sequence-like object could be dumped by either
# iterating over it using __iter__, or manually iterating
# over it using __len__ and __getitem__. As long as one or
# the other is implemented, this test will pass. The implementation
# is perhaps more lenient than it should be, as we don't ensure
# that all three methods are implemented correctly.
class MyArray:
def __iter__(self):
yield 0
yield 1
yield 1
def __getitem__(self, i):
return 0 if i == 0 else 1 # pragma: no cover
def __len__(self):
return 3 # pragma: no cover
self.assertEqual(json5.dumps(MyArray()), '[0, 1, 1]')
def test_invalid_collection(self):
# Check that something that isn't actually an array or a dict doesn't
# work.
self.assertRaises(TypeError, json5.dumps, {1, 2, 3})
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).
# pylint: disable=no-self-argument
class MyInt(int):
def __repr__(other): # pragma: no cover
del other
self.fail()
return ''
self.assertEqual(json5.dumps(MyInt(5)), '5')
class MyFloat(float):
def __repr__(other): # pragma: no cover
del other
self.fail()
return ''
self.assertEqual(json5.dumps(MyFloat(0.5)), '0.5')
def test_custom_objects(self):
class MyDict:
def __iter__(self): # pragma: no cover
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('\u00fc', '"\\u00fc"')
self.assertEqual(json5.dumps('\u00fc', ensure_ascii=False), '"\u00fc"')
def test_indent(self):
self.assertEqual(json5.dumps([1, 2, 3], indent=None), '[1, 2, 3]')
self.assertEqual(json5.dumps([1, 2, 3], indent=-1), '[\n1,\n2,\n3,\n]')
self.assertEqual(json5.dumps([1, 2, 3], indent=0), '[\n1,\n2,\n3,\n]')
self.assertEqual(json5.dumps([], indent=2), '[]')
self.assertEqual(
json5.dumps([1, 2, 3], indent=2), '[\n 1,\n 2,\n 3,\n]'
)
self.assertEqual(
json5.dumps([1, 2, 3], indent=' '), '[\n 1,\n 2,\n 3,\n]'
)
self.assertEqual(
json5.dumps([1, 2, 3], indent='++'), '[\n++1,\n++2,\n++3,\n]'
)
self.assertEqual(
json5.dumps([[1, 2, 3]], indent=2),
'[\n [\n 1,\n 2,\n 3,\n ],\n]',
)
self.assertEqual(json5.dumps({}, indent=2), '{}')
self.assertEqual(
json5.dumps({'foo': 'bar', 'baz': 'quux'}, indent=2),
'{\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}')
# pylint: disable=invalid-name
def test_identifiers_only_starting_with_reserved_words_are_not_quoted(
self,
):
self.check({'newbie': 1}, '{newbie: 1}')
# pylint: enable=invalid-name
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(
'\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):
self.check(chr(0x10000), '"\\ud800\\udc00"')
def test_empty_key(self):
self.assertEqual(json5.dumps({'': 'value'}), '{"": "value"}')
if __name__ == '__main__': # pragma: no cover
unittest.main()