##// END OF EJS Templates
Don't treat bytes objects as json-safe...
MinRK -
Show More
@@ -1,157 +1,161
1 1 """Utilities to manipulate JSON objects.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (C) 2010 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING.txt, distributed as part of this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13 # stdlib
14 14 import re
15 import sys
15 16 import types
16 17 from datetime import datetime
17 18
18 19 #-----------------------------------------------------------------------------
19 20 # Globals and constants
20 21 #-----------------------------------------------------------------------------
21 22
22 23 # timestamp formats
23 24 ISO8601="%Y-%m-%dT%H:%M:%S.%f"
24 25 ISO8601_PAT=re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+$")
25 26
26 27 #-----------------------------------------------------------------------------
27 28 # Classes and functions
28 29 #-----------------------------------------------------------------------------
29 30
30 31 def rekey(dikt):
31 32 """Rekey a dict that has been forced to use str keys where there should be
32 33 ints by json."""
33 34 for k in dikt.iterkeys():
34 35 if isinstance(k, basestring):
35 36 ik=fk=None
36 37 try:
37 38 ik = int(k)
38 39 except ValueError:
39 40 try:
40 41 fk = float(k)
41 42 except ValueError:
42 43 continue
43 44 if ik is not None:
44 45 nk = ik
45 46 else:
46 47 nk = fk
47 48 if nk in dikt:
48 49 raise KeyError("already have key %r"%nk)
49 50 dikt[nk] = dikt.pop(k)
50 51 return dikt
51 52
52 53
53 54 def extract_dates(obj):
54 55 """extract ISO8601 dates from unpacked JSON"""
55 56 if isinstance(obj, dict):
56 57 obj = dict(obj) # don't clobber
57 58 for k,v in obj.iteritems():
58 59 obj[k] = extract_dates(v)
59 60 elif isinstance(obj, (list, tuple)):
60 61 obj = [ extract_dates(o) for o in obj ]
61 62 elif isinstance(obj, basestring):
62 63 if ISO8601_PAT.match(obj):
63 64 obj = datetime.strptime(obj, ISO8601)
64 65 return obj
65 66
66 67 def squash_dates(obj):
67 68 """squash datetime objects into ISO8601 strings"""
68 69 if isinstance(obj, dict):
69 70 obj = dict(obj) # don't clobber
70 71 for k,v in obj.iteritems():
71 72 obj[k] = squash_dates(v)
72 73 elif isinstance(obj, (list, tuple)):
73 74 obj = [ squash_dates(o) for o in obj ]
74 75 elif isinstance(obj, datetime):
75 76 obj = obj.strftime(ISO8601)
76 77 return obj
77 78
78 79 def date_default(obj):
79 80 """default function for packing datetime objects in JSON."""
80 81 if isinstance(obj, datetime):
81 82 return obj.strftime(ISO8601)
82 83 else:
83 84 raise TypeError("%r is not JSON serializable"%obj)
84 85
85 86
86 87
87 88 def json_clean(obj):
88 89 """Clean an object to ensure it's safe to encode in JSON.
89 90
90 91 Atomic, immutable objects are returned unmodified. Sets and tuples are
91 92 converted to lists, lists are copied and dicts are also copied.
92 93
93 94 Note: dicts whose keys could cause collisions upon encoding (such as a dict
94 95 with both the number 1 and the string '1' as keys) will cause a ValueError
95 96 to be raised.
96 97
97 98 Parameters
98 99 ----------
99 100 obj : any python object
100 101
101 102 Returns
102 103 -------
103 104 out : object
104 105
105 106 A version of the input which will not cause an encoding error when
106 107 encoded as JSON. Note that this function does not *encode* its inputs,
107 108 it simply sanitizes it so that there will be no encoding errors later.
108 109
109 110 Examples
110 111 --------
111 112 >>> json_clean(4)
112 113 4
113 114 >>> json_clean(range(10))
114 115 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
115 116 >>> json_clean(dict(x=1, y=2))
116 117 {'y': 2, 'x': 1}
117 118 >>> json_clean(dict(x=1, y=2, z=[1,2,3]))
118 119 {'y': 2, 'x': 1, 'z': [1, 2, 3]}
119 120 >>> json_clean(True)
120 121 True
121 122 """
122 123 # types that are 'atomic' and ok in json as-is. bool doesn't need to be
123 124 # listed explicitly because bools pass as int instances
124 atomic_ok = (basestring, int, float, types.NoneType)
125 atomic_ok = (unicode, int, float, types.NoneType)
125 126
126 127 # containers that we need to convert into lists
127 128 container_to_list = (tuple, set, types.GeneratorType)
128 129
129 130 if isinstance(obj, atomic_ok):
130 131 return obj
131 132
133 if isinstance(obj, bytes):
134 return obj.decode(sys.getdefaultencoding(), 'replace')
135
132 136 if isinstance(obj, container_to_list) or (
133 137 hasattr(obj, '__iter__') and hasattr(obj, 'next')):
134 138 obj = list(obj)
135 139
136 140 if isinstance(obj, list):
137 141 return [json_clean(x) for x in obj]
138 142
139 143 if isinstance(obj, dict):
140 144 # First, validate that the dict won't lose data in conversion due to
141 145 # key collisions after stringification. This can happen with keys like
142 146 # True and 'true' or 1 and '1', which collide in JSON.
143 147 nkeys = len(obj)
144 148 nkeys_collapsed = len(set(map(str, obj)))
145 149 if nkeys != nkeys_collapsed:
146 150 raise ValueError('dict can not be safely converted to JSON: '
147 151 'key collision would lead to dropped values')
148 152 # If all OK, proceed by making the new dict that will be json-safe
149 153 out = {}
150 154 for k,v in obj.iteritems():
151 155 out[str(k)] = json_clean(v)
152 156 return out
153 157
154 158 # If we get here, we don't know how to handle the object, so we just get
155 159 # its repr and return that. This will catch lambdas, open sockets, class
156 160 # objects, and any other complicated contraption that json can't encode
157 161 return repr(obj)
General Comments 0
You need to be logged in to leave comments. Login now