##// END OF EJS Templates
use `isoformat()` in jsonutil...
MinRK -
Show More
@@ -1,224 +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+$")
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 obj = datetime.strptime(obj, ISO8601)
75 # FIXME: add actual timezone support
76 notz = obj.split('Z',1)[0]
77 obj = datetime.strptime(notz, ISO8601)
76 78 return obj
77 79
78 80 def squash_dates(obj):
79 81 """squash datetime objects into ISO8601 strings"""
80 82 if isinstance(obj, dict):
81 83 obj = dict(obj) # don't clobber
82 84 for k,v in obj.iteritems():
83 85 obj[k] = squash_dates(v)
84 86 elif isinstance(obj, (list, tuple)):
85 87 obj = [ squash_dates(o) for o in obj ]
86 88 elif isinstance(obj, datetime):
87 obj = obj.strftime(ISO8601)
89 obj = obj.isoformat()
88 90 return obj
89 91
90 92 def date_default(obj):
91 93 """default function for packing datetime objects in JSON."""
92 94 if isinstance(obj, datetime):
93 return obj.strftime(ISO8601)
95 return obj.isoformat()
94 96 else:
95 97 raise TypeError("%r is not JSON serializable"%obj)
96 98
97 99
98 100 # constants for identifying png/jpeg data
99 101 PNG = b'\x89PNG\r\n\x1a\n'
100 102 # front of PNG base64-encoded
101 103 PNG64 = b'iVBORw0KG'
102 104 JPEG = b'\xff\xd8'
103 105 # front of JPEG base64-encoded
104 106 JPEG64 = b'/9'
105 107
106 108 def encode_images(format_dict):
107 109 """b64-encodes images in a displaypub format dict
108 110
109 111 Perhaps this should be handled in json_clean itself?
110 112
111 113 Parameters
112 114 ----------
113 115
114 116 format_dict : dict
115 117 A dictionary of display data keyed by mime-type
116 118
117 119 Returns
118 120 -------
119 121
120 122 format_dict : dict
121 123 A copy of the same dictionary,
122 124 but binary image data ('image/png' or 'image/jpeg')
123 125 is base64-encoded.
124 126
125 127 """
126 128 encoded = format_dict.copy()
127 129
128 130 pngdata = format_dict.get('image/png')
129 131 if isinstance(pngdata, bytes):
130 132 # make sure we don't double-encode
131 133 if not pngdata.startswith(PNG64):
132 134 pngdata = encodebytes(pngdata)
133 135 encoded['image/png'] = pngdata.decode('ascii')
134 136
135 137 jpegdata = format_dict.get('image/jpeg')
136 138 if isinstance(jpegdata, bytes):
137 139 # make sure we don't double-encode
138 140 if not jpegdata.startswith(JPEG64):
139 141 jpegdata = encodebytes(jpegdata)
140 142 encoded['image/jpeg'] = jpegdata.decode('ascii')
141 143
142 144 return encoded
143 145
144 146
145 147 def json_clean(obj):
146 148 """Clean an object to ensure it's safe to encode in JSON.
147 149
148 150 Atomic, immutable objects are returned unmodified. Sets and tuples are
149 151 converted to lists, lists are copied and dicts are also copied.
150 152
151 153 Note: dicts whose keys could cause collisions upon encoding (such as a dict
152 154 with both the number 1 and the string '1' as keys) will cause a ValueError
153 155 to be raised.
154 156
155 157 Parameters
156 158 ----------
157 159 obj : any python object
158 160
159 161 Returns
160 162 -------
161 163 out : object
162 164
163 165 A version of the input which will not cause an encoding error when
164 166 encoded as JSON. Note that this function does not *encode* its inputs,
165 167 it simply sanitizes it so that there will be no encoding errors later.
166 168
167 169 Examples
168 170 --------
169 171 >>> json_clean(4)
170 172 4
171 173 >>> json_clean(range(10))
172 174 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
173 175 >>> sorted(json_clean(dict(x=1, y=2)).items())
174 176 [('x', 1), ('y', 2)]
175 177 >>> sorted(json_clean(dict(x=1, y=2, z=[1,2,3])).items())
176 178 [('x', 1), ('y', 2), ('z', [1, 2, 3])]
177 179 >>> json_clean(True)
178 180 True
179 181 """
180 182 # types that are 'atomic' and ok in json as-is. bool doesn't need to be
181 183 # listed explicitly because bools pass as int instances
182 184 atomic_ok = (unicode, int, types.NoneType)
183 185
184 186 # containers that we need to convert into lists
185 187 container_to_list = (tuple, set, types.GeneratorType)
186 188
187 189 if isinstance(obj, float):
188 190 # cast out-of-range floats to their reprs
189 191 if math.isnan(obj) or math.isinf(obj):
190 192 return repr(obj)
191 193 return obj
192 194
193 195 if isinstance(obj, atomic_ok):
194 196 return obj
195 197
196 198 if isinstance(obj, bytes):
197 199 return obj.decode(DEFAULT_ENCODING, 'replace')
198 200
199 201 if isinstance(obj, container_to_list) or (
200 202 hasattr(obj, '__iter__') and hasattr(obj, next_attr_name)):
201 203 obj = list(obj)
202 204
203 205 if isinstance(obj, list):
204 206 return [json_clean(x) for x in obj]
205 207
206 208 if isinstance(obj, dict):
207 209 # First, validate that the dict won't lose data in conversion due to
208 210 # key collisions after stringification. This can happen with keys like
209 211 # True and 'true' or 1 and '1', which collide in JSON.
210 212 nkeys = len(obj)
211 213 nkeys_collapsed = len(set(map(str, obj)))
212 214 if nkeys != nkeys_collapsed:
213 215 raise ValueError('dict can not be safely converted to JSON: '
214 216 'key collision would lead to dropped values')
215 217 # If all OK, proceed by making the new dict that will be json-safe
216 218 out = {}
217 219 for k,v in obj.iteritems():
218 220 out[str(k)] = json_clean(v)
219 221 return out
220 222
221 223 # If we get here, we don't know how to handle the object, so we just get
222 224 # its repr and return that. This will catch lambdas, open sockets, class
223 225 # objects, and any other complicated contraption that json can't encode
224 226 return repr(obj)
General Comments 0
You need to be logged in to leave comments. Login now