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