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