Skip to content

Commit 91852ca

Browse files
committed
Issue 5381: Add object_pairs_hook to the json module.
1 parent 2124599 commit 91852ca

7 files changed

Lines changed: 150 additions & 23 deletions

File tree

Doc/library/json.rst

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Basic Usage
166166
:func:`dump`.
167167

168168

169-
.. function:: load(fp[, encoding[, cls[, object_hook[, parse_float[, parse_int[, parse_constant[, **kw]]]]]]])
169+
.. function:: load(fp[, encoding[, cls[, object_hook[, parse_float[, parse_int[, parse_constant[, object_pairs_hook[, **kw]]]]]]]])
170170

171171
Deserialize *fp* (a ``.read()``-supporting file-like object containing a JSON
172172
document) to a Python object.
@@ -182,6 +182,17 @@ Basic Usage
182182
*object_hook* will be used instead of the :class:`dict`. This feature can be used
183183
to implement custom decoders (e.g. JSON-RPC class hinting).
184184

185+
*object_pairs_hook* is an optional function that will be called with the
186+
result of any object literal decode with an ordered list of pairs. The
187+
return value of *object_pairs_hook* will be used instead of the
188+
:class:`dict`. This feature can be used to implement custom decoders that
189+
rely on the order that the key and value pairs are decoded (for example,
190+
:func:`collections.OrderedDict` will remember the order of insertion). If
191+
*object_hook* is also defined, the *object_pairs_hook* takes priority.
192+
193+
.. versionchanged:: 2.7
194+
Added support for *object_pairs_hook*.
195+
185196
*parse_float*, if specified, will be called with the string of every JSON
186197
float to be decoded. By default, this is equivalent to ``float(num_str)``.
187198
This can be used to use another datatype or parser for JSON floats
@@ -202,7 +213,7 @@ Basic Usage
202213
class.
203214

204215

205-
.. function:: loads(s[, encoding[, cls[, object_hook[, parse_float[, parse_int[, parse_constant[, **kw]]]]]]])
216+
.. function:: loads(s[, encoding[, cls[, object_hook[, parse_float[, parse_int[, parse_constant[, object_pairs_hook[, **kw]]]]]]]])
206217

207218
Deserialize *s* (a :class:`str` or :class:`unicode` instance containing a JSON
208219
document) to a Python object.
@@ -218,7 +229,7 @@ Basic Usage
218229
Encoders and decoders
219230
---------------------
220231

221-
.. class:: JSONDecoder([encoding[, object_hook[, parse_float[, parse_int[, parse_constant[, strict]]]]]])
232+
.. class:: JSONDecoder([encoding[, object_hook[, parse_float[, parse_int[, parse_constant[, strict[, object_pairs_hook]]]]]]])
222233

223234
Simple JSON decoder.
224235

@@ -259,6 +270,17 @@ Encoders and decoders
259270
:class:`dict`. This can be used to provide custom deserializations (e.g. to
260271
support JSON-RPC class hinting).
261272

273+
*object_pairs_hook*, if specified will be called with the result of every
274+
JSON object decoded with an ordered list of pairs. The return value of
275+
*object_pairs_hook* will be used instead of the :class:`dict`. This
276+
feature can be used to implement custom decoders that rely on the order
277+
that the key and value pairs are decoded (for example,
278+
:func:`collections.OrderedDict` will remember the order of insertion). If
279+
*object_hook* is also defined, the *object_pairs_hook* takes priority.
280+
281+
.. versionchanged:: 2.7
282+
Added support for *object_pairs_hook*.
283+
262284
*parse_float*, if specified, will be called with the string of every JSON
263285
float to be decoded. By default, this is equivalent to ``float(num_str)``.
264286
This can be used to use another datatype or parser for JSON floats

Lib/json/__init__.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,11 +238,12 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
238238
**kw).encode(obj)
239239

240240

241-
_default_decoder = JSONDecoder(encoding=None, object_hook=None)
241+
_default_decoder = JSONDecoder(encoding=None, object_hook=None,
242+
object_pairs_hook=None)
242243

243244

244245
def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None,
245-
parse_int=None, parse_constant=None, **kw):
246+
parse_int=None, parse_constant=None, object_pairs_hook=None, **kw):
246247
"""Deserialize ``fp`` (a ``.read()``-supporting file-like object containing
247248
a JSON document) to a Python object.
248249
@@ -265,11 +266,11 @@ def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None,
265266
return loads(fp.read(),
266267
encoding=encoding, cls=cls, object_hook=object_hook,
267268
parse_float=parse_float, parse_int=parse_int,
268-
parse_constant=parse_constant, **kw)
269+
parse_constant=parse_constant, object_pairs_hook=None, **kw)
269270

270271

271272
def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None,
272-
parse_int=None, parse_constant=None, **kw):
273+
parse_int=None, parse_constant=None, object_pairs_hook=None, **kw):
273274
"""Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON
274275
document) to a Python object.
275276
@@ -304,12 +305,14 @@ def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None,
304305
"""
305306
if (cls is None and encoding is None and object_hook is None and
306307
parse_int is None and parse_float is None and
307-
parse_constant is None and not kw):
308+
parse_constant is None and object_pairs_hook is None and not kw):
308309
return _default_decoder.decode(s)
309310
if cls is None:
310311
cls = JSONDecoder
311312
if object_hook is not None:
312313
kw['object_hook'] = object_hook
314+
if object_pairs_hook is not None:
315+
kw['object_pairs_hook'] = object_pairs_hook
313316
if parse_float is not None:
314317
kw['parse_float'] = parse_float
315318
if parse_int is not None:

Lib/json/decoder.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,9 @@ def py_scanstring(s, end, encoding=None, strict=True,
147147
WHITESPACE_STR = ' \t\n\r'
148148

149149
def JSONObject((s, end), encoding, strict, scan_once, object_hook,
150-
_w=WHITESPACE.match, _ws=WHITESPACE_STR):
151-
pairs = {}
150+
object_pairs_hook, _w=WHITESPACE.match, _ws=WHITESPACE_STR):
151+
pairs = []
152+
pairs_append = pairs.append
152153
# Use a slice to prevent IndexError from being raised, the following
153154
# check will raise a more specific ValueError if the string is empty
154155
nextchar = s[end:end + 1]
@@ -187,7 +188,7 @@ def JSONObject((s, end), encoding, strict, scan_once, object_hook,
187188
value, end = scan_once(s, end)
188189
except StopIteration:
189190
raise ValueError(errmsg("Expecting object", s, end))
190-
pairs[key] = value
191+
pairs_append((key, value))
191192

192193
try:
193194
nextchar = s[end]
@@ -218,6 +219,10 @@ def JSONObject((s, end), encoding, strict, scan_once, object_hook,
218219
if nextchar != '"':
219220
raise ValueError(errmsg("Expecting property name", s, end - 1))
220221

222+
if object_pairs_hook is not None:
223+
result = object_pairs_hook(pairs)
224+
return result, end
225+
pairs = dict(pairs)
221226
if object_hook is not None:
222227
pairs = object_hook(pairs)
223228
return pairs, end
@@ -289,7 +294,8 @@ class JSONDecoder(object):
289294
"""
290295

291296
def __init__(self, encoding=None, object_hook=None, parse_float=None,
292-
parse_int=None, parse_constant=None, strict=True):
297+
parse_int=None, parse_constant=None, strict=True,
298+
object_pairs_hook=None):
293299
"""``encoding`` determines the encoding used to interpret any ``str``
294300
objects decoded by this instance (utf-8 by default). It has no
295301
effect when decoding ``unicode`` objects.
@@ -320,6 +326,7 @@ def __init__(self, encoding=None, object_hook=None, parse_float=None,
320326
"""
321327
self.encoding = encoding
322328
self.object_hook = object_hook
329+
self.object_pairs_hook = object_pairs_hook
323330
self.parse_float = parse_float or float
324331
self.parse_int = parse_int or int
325332
self.parse_constant = parse_constant or _CONSTANTS.__getitem__

Lib/json/tests/test_decode.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from unittest import TestCase
33

44
import json
5+
from collections import OrderedDict
56

67
class TestDecode(TestCase):
78
def test_decimal(self):
@@ -20,3 +21,18 @@ def test_decoder_optimizations(self):
2021
# exercise the uncommon cases. The array cases are already covered.
2122
rval = json.loads('{ "key" : "value" , "k":"v" }')
2223
self.assertEquals(rval, {"key":"value", "k":"v"})
24+
25+
def test_object_pairs_hook(self):
26+
s = '{"xkd":1, "kcw":2, "art":3, "hxm":4, "qrt":5, "pad":6, "hoy":7}'
27+
p = [("xkd", 1), ("kcw", 2), ("art", 3), ("hxm", 4),
28+
("qrt", 5), ("pad", 6), ("hoy", 7)]
29+
self.assertEqual(json.loads(s), eval(s))
30+
self.assertEqual(json.loads(s, object_pairs_hook = lambda x: x), p)
31+
od = json.loads(s, object_pairs_hook = OrderedDict)
32+
self.assertEqual(od, OrderedDict(p))
33+
self.assertEqual(type(od), OrderedDict)
34+
# the object_pairs_hook takes priority over the object_hook
35+
self.assertEqual(json.loads(s,
36+
object_pairs_hook = OrderedDict,
37+
object_hook = lambda x: None),
38+
OrderedDict(p))

Lib/json/tests/test_unicode.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from unittest import TestCase
22

33
import json
4+
from collections import OrderedDict
45

56
class TestUnicode(TestCase):
67
def test_encoding1(self):
@@ -54,6 +55,21 @@ def test_unicode_decode(self):
5455
s = '"\\u{0:04x}"'.format(i)
5556
self.assertEquals(json.loads(s), u)
5657

58+
def test_object_pairs_hook_with_unicode(self):
59+
s = u'{"xkd":1, "kcw":2, "art":3, "hxm":4, "qrt":5, "pad":6, "hoy":7}'
60+
p = [(u"xkd", 1), (u"kcw", 2), (u"art", 3), (u"hxm", 4),
61+
(u"qrt", 5), (u"pad", 6), (u"hoy", 7)]
62+
self.assertEqual(json.loads(s), eval(s))
63+
self.assertEqual(json.loads(s, object_pairs_hook = lambda x: x), p)
64+
od = json.loads(s, object_pairs_hook = OrderedDict)
65+
self.assertEqual(od, OrderedDict(p))
66+
self.assertEqual(type(od), OrderedDict)
67+
# the object_pairs_hook takes priority over the object_hook
68+
self.assertEqual(json.loads(s,
69+
object_pairs_hook = OrderedDict,
70+
object_hook = lambda x: None),
71+
OrderedDict(p))
72+
5773
def test_default_encoding(self):
5874
self.assertEquals(json.loads(u'{"a": "\xe9"}'.encode('utf-8')),
5975
{'a': u'\xe9'})

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ Core and Builtins
181181
Library
182182
-------
183183

184+
- Issue #5381: Added object_pairs_hook to the json module. This allows
185+
OrderedDicts to be built by the decoder.
186+
184187
- Issue #2110: Add support for thousands separator and 'n' type
185188
specifier to Decimal.__format__
186189

0 commit comments

Comments
 (0)