##// END OF EJS Templates
Created JSON-safety utilities....
Fernando Perez -
Show More
@@ -0,0 +1,90 b''
1 """Utilities to manipulate JSON objects.
2 """
3 #-----------------------------------------------------------------------------
4 # Copyright (C) 2010 The IPython Development Team
5 #
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING.txt, distributed as part of this software.
8 #-----------------------------------------------------------------------------
9
10 #-----------------------------------------------------------------------------
11 # Imports
12 #-----------------------------------------------------------------------------
13 # stdlib
14 import types
15
16 #-----------------------------------------------------------------------------
17 # Classes and functions
18 #-----------------------------------------------------------------------------
19
20 def json_clean(obj):
21 """Clean an object to ensure it's safe to encode in JSON.
22
23 Atomic, immutable objects are returned unmodified. Sets and tuples are
24 converted to lists, lists are copied and dicts are also copied.
25
26 Note: dicts whose keys could cause collisions upon encoding (such as a dict
27 with both the number 1 and the string '1' as keys) will cause a ValueError
28 to be raised.
29
30 Parameters
31 ----------
32 obj : any python object
33
34 Returns
35 -------
36 out : object
37
38 A version of the input which will not cause an encoding error when
39 encoded as JSON. Note that this function does not *encode* its inputs,
40 it simply sanitizes it so that there will be no encoding errors later.
41
42 Examples
43 --------
44 >>> json_clean(4)
45 4
46 >>> json_clean(range(10))
47 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
48 >>> json_clean(dict(x=1, y=2))
49 {'y': 2, 'x': 1}
50 >>> json_clean(dict(x=1, y=2, z=[1,2,3]))
51 {'y': 2, 'x': 1, 'z': [1, 2, 3]}
52 >>> json_clean(True)
53 True
54 """
55 # types that are 'atomic' and ok in json as-is. bool doesn't need to be
56 # listed explicitly because bools pass as int instances
57 atomic_ok = (basestring, int, float, types.NoneType)
58
59 # containers that we need to convert into lists
60 container_to_list = (tuple, set, types.GeneratorType)
61
62 if isinstance(obj, atomic_ok):
63 return obj
64
65 if isinstance(obj, container_to_list) or (
66 hasattr(obj, '__iter__') and hasattr(obj, 'next')):
67 obj = list(obj)
68
69 if isinstance(obj, list):
70 return [json_clean(x) for x in obj]
71
72 if isinstance(obj, dict):
73 # First, validate that the dict won't lose data in conversion due to
74 # key collisions after stringification. This can happen with keys like
75 # True and 'true' or 1 and '1', which collide in JSON.
76 nkeys = len(obj)
77 nkeys_collapsed = len(set(map(str, obj)))
78 if nkeys != nkeys_collapsed:
79 raise ValueError('dict can not be safely converted to JSON: '
80 'key collision would lead to dropped values')
81 # If all OK, proceed by making the new dict that will be json-safe
82 out = {}
83 for k,v in obj.iteritems():
84 out[str(k)] = json_clean(v)
85 return out
86
87 # If we get here, we don't know how to handle the object, so we just get
88 # its repr and return that. This will catch lambdas, open sockets, class
89 # objects, and any other complicated contraption that json can't encode
90 return repr(obj)
@@ -0,0 +1,71 b''
1 """Test suite for our JSON utilities.
2 """
3 #-----------------------------------------------------------------------------
4 # Copyright (C) 2010 The IPython Development Team
5 #
6 # Distributed under the terms of the BSD License. The full license is in
7 # the file COPYING.txt, distributed as part of this software.
8 #-----------------------------------------------------------------------------
9
10 #-----------------------------------------------------------------------------
11 # Imports
12 #-----------------------------------------------------------------------------
13 # stdlib
14 import json
15
16 # third party
17 import nose.tools as nt
18
19 # our own
20 from ..jsonutil import json_clean
21
22 #-----------------------------------------------------------------------------
23 # Test functions
24 #-----------------------------------------------------------------------------
25
26 def test():
27 # list of input/expected output. Use None for the expected output if it
28 # can be the same as the input.
29 pairs = [(1, None), # start with scalars
30 (1.0, None),
31 ('a', None),
32 (True, None),
33 (False, None),
34 (None, None),
35 # complex numbers for now just go to strings, as otherwise they
36 # are unserializable
37 (1j, '1j'),
38 # Containers
39 ([1, 2], None),
40 ((1, 2), [1, 2]),
41 (set([1, 2]), [1, 2]),
42 (dict(x=1), None),
43 ({'x': 1, 'y':[1,2,3], '1':'int'}, None),
44 # More exotic objects
45 ((x for x in range(3)), [0, 1, 2]),
46 (iter([1, 2]), [1, 2]),
47 ]
48
49 for val, jval in pairs:
50 if jval is None:
51 jval = val
52 out = json_clean(val)
53 # validate our cleanup
54 nt.assert_equal(out, jval)
55 # and ensure that what we return, indeed encodes cleanly
56 json.loads(json.dumps(out))
57
58
59 def test_lambda():
60 jc = json_clean(lambda : 1)
61 nt.assert_true(jc.startswith('<function <lambda> at '))
62 json.dumps(jc)
63
64
65 def test_exception():
66 bad_dicts = [{1:'number', '1':'string'},
67 {True:'bool', 'True':'string'},
68 ]
69 for d in bad_dicts:
70 nt.assert_raises(ValueError, json_clean, d)
71
General Comments 0
You need to be logged in to leave comments. Login now