##// END OF EJS Templates
allow just the trailing Z for timezone
MinRK -
Show More
@@ -1,226 +1,226 b''
1 1 """Utilities to manipulate JSON objects.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (C) 2010-2011 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 math
15 15 import re
16 16 import types
17 17 from datetime import datetime
18 18
19 19 try:
20 20 # base64.encodestring is deprecated in Python 3.x
21 21 from base64 import encodebytes
22 22 except ImportError:
23 23 # Python 2.x
24 24 from base64 import encodestring as encodebytes
25 25
26 26 from IPython.utils import py3compat
27 27 from IPython.utils.encoding import DEFAULT_ENCODING
28 28 next_attr_name = '__next__' if py3compat.PY3 else 'next'
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Globals and constants
32 32 #-----------------------------------------------------------------------------
33 33
34 34 # timestamp formats
35 35 ISO8601="%Y-%m-%dT%H:%M:%S.%f"
36 ISO8601_PAT=re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+(Z?[\+\-]\d{2}:?\d{2})?$")
36 ISO8601_PAT=re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z?([\+\-]\d{2}:?\d{2})?$")
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Classes and functions
40 40 #-----------------------------------------------------------------------------
41 41
42 42 def rekey(dikt):
43 43 """Rekey a dict that has been forced to use str keys where there should be
44 44 ints by json."""
45 45 for k in dikt.iterkeys():
46 46 if isinstance(k, basestring):
47 47 ik=fk=None
48 48 try:
49 49 ik = int(k)
50 50 except ValueError:
51 51 try:
52 52 fk = float(k)
53 53 except ValueError:
54 54 continue
55 55 if ik is not None:
56 56 nk = ik
57 57 else:
58 58 nk = fk
59 59 if nk in dikt:
60 60 raise KeyError("already have key %r"%nk)
61 61 dikt[nk] = dikt.pop(k)
62 62 return dikt
63 63
64 64
65 65 def extract_dates(obj):
66 66 """extract ISO8601 dates from unpacked JSON"""
67 67 if isinstance(obj, dict):
68 68 obj = dict(obj) # don't clobber
69 69 for k,v in obj.iteritems():
70 70 obj[k] = extract_dates(v)
71 71 elif isinstance(obj, (list, tuple)):
72 72 obj = [ extract_dates(o) for o in obj ]
73 73 elif isinstance(obj, basestring):
74 74 if ISO8601_PAT.match(obj):
75 75 # FIXME: add actual timezone support
76 76 notz = obj.split('Z',1)[0]
77 77 obj = datetime.strptime(notz, ISO8601)
78 78 return obj
79 79
80 80 def squash_dates(obj):
81 81 """squash datetime objects into ISO8601 strings"""
82 82 if isinstance(obj, dict):
83 83 obj = dict(obj) # don't clobber
84 84 for k,v in obj.iteritems():
85 85 obj[k] = squash_dates(v)
86 86 elif isinstance(obj, (list, tuple)):
87 87 obj = [ squash_dates(o) for o in obj ]
88 88 elif isinstance(obj, datetime):
89 89 obj = obj.isoformat()
90 90 return obj
91 91
92 92 def date_default(obj):
93 93 """default function for packing datetime objects in JSON."""
94 94 if isinstance(obj, datetime):
95 95 return obj.isoformat()
96 96 else:
97 97 raise TypeError("%r is not JSON serializable"%obj)
98 98
99 99
100 100 # constants for identifying png/jpeg data
101 101 PNG = b'\x89PNG\r\n\x1a\n'
102 102 # front of PNG base64-encoded
103 103 PNG64 = b'iVBORw0KG'
104 104 JPEG = b'\xff\xd8'
105 105 # front of JPEG base64-encoded
106 106 JPEG64 = b'/9'
107 107
108 108 def encode_images(format_dict):
109 109 """b64-encodes images in a displaypub format dict
110 110
111 111 Perhaps this should be handled in json_clean itself?
112 112
113 113 Parameters
114 114 ----------
115 115
116 116 format_dict : dict
117 117 A dictionary of display data keyed by mime-type
118 118
119 119 Returns
120 120 -------
121 121
122 122 format_dict : dict
123 123 A copy of the same dictionary,
124 124 but binary image data ('image/png' or 'image/jpeg')
125 125 is base64-encoded.
126 126
127 127 """
128 128 encoded = format_dict.copy()
129 129
130 130 pngdata = format_dict.get('image/png')
131 131 if isinstance(pngdata, bytes):
132 132 # make sure we don't double-encode
133 133 if not pngdata.startswith(PNG64):
134 134 pngdata = encodebytes(pngdata)
135 135 encoded['image/png'] = pngdata.decode('ascii')
136 136
137 137 jpegdata = format_dict.get('image/jpeg')
138 138 if isinstance(jpegdata, bytes):
139 139 # make sure we don't double-encode
140 140 if not jpegdata.startswith(JPEG64):
141 141 jpegdata = encodebytes(jpegdata)
142 142 encoded['image/jpeg'] = jpegdata.decode('ascii')
143 143
144 144 return encoded
145 145
146 146
147 147 def json_clean(obj):
148 148 """Clean an object to ensure it's safe to encode in JSON.
149 149
150 150 Atomic, immutable objects are returned unmodified. Sets and tuples are
151 151 converted to lists, lists are copied and dicts are also copied.
152 152
153 153 Note: dicts whose keys could cause collisions upon encoding (such as a dict
154 154 with both the number 1 and the string '1' as keys) will cause a ValueError
155 155 to be raised.
156 156
157 157 Parameters
158 158 ----------
159 159 obj : any python object
160 160
161 161 Returns
162 162 -------
163 163 out : object
164 164
165 165 A version of the input which will not cause an encoding error when
166 166 encoded as JSON. Note that this function does not *encode* its inputs,
167 167 it simply sanitizes it so that there will be no encoding errors later.
168 168
169 169 Examples
170 170 --------
171 171 >>> json_clean(4)
172 172 4
173 173 >>> json_clean(range(10))
174 174 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
175 175 >>> sorted(json_clean(dict(x=1, y=2)).items())
176 176 [('x', 1), ('y', 2)]
177 177 >>> sorted(json_clean(dict(x=1, y=2, z=[1,2,3])).items())
178 178 [('x', 1), ('y', 2), ('z', [1, 2, 3])]
179 179 >>> json_clean(True)
180 180 True
181 181 """
182 182 # types that are 'atomic' and ok in json as-is. bool doesn't need to be
183 183 # listed explicitly because bools pass as int instances
184 184 atomic_ok = (unicode, int, types.NoneType)
185 185
186 186 # containers that we need to convert into lists
187 187 container_to_list = (tuple, set, types.GeneratorType)
188 188
189 189 if isinstance(obj, float):
190 190 # cast out-of-range floats to their reprs
191 191 if math.isnan(obj) or math.isinf(obj):
192 192 return repr(obj)
193 193 return obj
194 194
195 195 if isinstance(obj, atomic_ok):
196 196 return obj
197 197
198 198 if isinstance(obj, bytes):
199 199 return obj.decode(DEFAULT_ENCODING, 'replace')
200 200
201 201 if isinstance(obj, container_to_list) or (
202 202 hasattr(obj, '__iter__') and hasattr(obj, next_attr_name)):
203 203 obj = list(obj)
204 204
205 205 if isinstance(obj, list):
206 206 return [json_clean(x) for x in obj]
207 207
208 208 if isinstance(obj, dict):
209 209 # First, validate that the dict won't lose data in conversion due to
210 210 # key collisions after stringification. This can happen with keys like
211 211 # True and 'true' or 1 and '1', which collide in JSON.
212 212 nkeys = len(obj)
213 213 nkeys_collapsed = len(set(map(str, obj)))
214 214 if nkeys != nkeys_collapsed:
215 215 raise ValueError('dict can not be safely converted to JSON: '
216 216 'key collision would lead to dropped values')
217 217 # If all OK, proceed by making the new dict that will be json-safe
218 218 out = {}
219 219 for k,v in obj.iteritems():
220 220 out[str(k)] = json_clean(v)
221 221 return out
222 222
223 223 # If we get here, we don't know how to handle the object, so we just get
224 224 # its repr and return that. This will catch lambdas, open sockets, class
225 225 # objects, and any other complicated contraption that json can't encode
226 226 return repr(obj)
General Comments 0
You need to be logged in to leave comments. Login now