diff --git a/Lib/json/__init__.py b/Lib/json/__init__.py index 256e76a0a67f8f..52384da5812b47 100644 --- a/Lib/json/__init__.py +++ b/Lib/json/__init__.py @@ -115,11 +115,12 @@ indent=None, separators=None, default=None, + encode_float=None, ) def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, - default=None, sort_keys=False, **kw): + default=None, sort_keys=False, encode_float=None, **kw): """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a ``.write()``-supporting file-like object). @@ -156,6 +157,9 @@ def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, If *sort_keys* is true (default: ``False``), then the output of dictionaries will be sorted by key. + If specified, ``encode_float`` is a function that will be called when encoding + float into string. It should return a string. + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the ``.default()`` method to serialize additional types), specify it with the ``cls`` kwarg; otherwise ``JSONEncoder`` is used. @@ -165,7 +169,7 @@ def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, if (not skipkeys and ensure_ascii and check_circular and allow_nan and cls is None and indent is None and separators is None and - default is None and not sort_keys and not kw): + default is None and not sort_keys and not encode_float and not kw): iterable = _default_encoder.iterencode(obj) else: if cls is None: @@ -173,7 +177,8 @@ def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular, allow_nan=allow_nan, indent=indent, separators=separators, - default=default, sort_keys=sort_keys, **kw).iterencode(obj) + default=default, sort_keys=sort_keys, encode_float=encode_float, + **kw).iterencode(obj) # could accelerate with writelines in some versions of Python, at # a debuggability cost for chunk in iterable: @@ -182,7 +187,7 @@ def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, - default=None, sort_keys=False, **kw): + default=None, sort_keys=False, encode_float=None, **kw): """Serialize ``obj`` to a JSON formatted ``str``. If ``skipkeys`` is true then ``dict`` keys that are not basic types @@ -218,6 +223,9 @@ def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, If *sort_keys* is true (default: ``False``), then the output of dictionaries will be sorted by key. + If specified, ``encode_float`` is a function that will be called when encoding + float into string. It should return a string. + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the ``.default()`` method to serialize additional types), specify it with the ``cls`` kwarg; otherwise ``JSONEncoder`` is used. @@ -227,7 +235,7 @@ def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, if (not skipkeys and ensure_ascii and check_circular and allow_nan and cls is None and indent is None and separators is None and - default is None and not sort_keys and not kw): + default is None and not sort_keys and not encode_float and not kw): return _default_encoder.encode(obj) if cls is None: cls = JSONEncoder @@ -235,7 +243,7 @@ def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular, allow_nan=allow_nan, indent=indent, separators=separators, default=default, sort_keys=sort_keys, - **kw).encode(obj) + encode_float=encode_float, **kw).encode(obj) _default_decoder = JSONDecoder(object_hook=None, object_pairs_hook=None) diff --git a/Lib/json/encoder.py b/Lib/json/encoder.py index 45f547741885a8..7385cc8c639608 100644 --- a/Lib/json/encoder.py +++ b/Lib/json/encoder.py @@ -104,7 +104,7 @@ class JSONEncoder(object): key_separator = ': ' def __init__(self, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, - indent=None, separators=None, default=None): + indent=None, separators=None, default=None, encode_float=None): """Constructor for JSONEncoder, with sensible defaults. If skipkeys is false, then it is a TypeError to attempt @@ -143,6 +143,9 @@ def __init__(self, *, skipkeys=False, ensure_ascii=True, that can't otherwise be serialized. It should return a JSON encodable version of the object or raise a ``TypeError``. + If specified, encode_float is a function that will be called when encoding + float into string. It should return a string. + """ self.skipkeys = skipkeys @@ -151,6 +154,8 @@ def __init__(self, *, skipkeys=False, ensure_ascii=True, self.allow_nan = allow_nan self.sort_keys = sort_keys self.indent = indent + self.encode_float = encode_float + if separators is not None: self.item_separator, self.key_separator = separators elif indent is not None: @@ -221,42 +226,46 @@ def iterencode(self, o, _one_shot=False): else: _encoder = encode_basestring - def floatstr(o, allow_nan=self.allow_nan, - _repr=float.__repr__, _inf=INFINITY, _neginf=-INFINITY): - # Check for specials. Note that this type of test is processor - # and/or platform-specific, so do tests which don't depend on the - # internals. - - if o != o: - text = 'NaN' - elif o == _inf: - text = 'Infinity' - elif o == _neginf: - text = '-Infinity' - else: - return _repr(o) - - if not allow_nan: - raise ValueError( - "Out of range float values are not JSON compliant: " + - repr(o)) - - return text - if (_one_shot and c_make_encoder is not None and self.indent is None): _iterencode = c_make_encoder( - markers, self.default, _encoder, self.indent, + markers, self.default, _encoder, self.indent, self.encode_float, self.key_separator, self.item_separator, self.sort_keys, self.skipkeys, self.allow_nan) else: + if not self.encode_float: + def floatstr(o, allow_nan=self.allow_nan, + _repr=float.__repr__, _inf=INFINITY, _neginf=-INFINITY): + # Check for specials. Note that this type of test is processor + # and/or platform-specific, so do tests which don't depend on the + # internals. + + if o != o: + text = 'NaN' + elif o == _inf: + text = 'Infinity' + elif o == _neginf: + text = '-Infinity' + else: + return _repr(o) + + if not allow_nan: + raise ValueError( + "Out of range float values are not JSON compliant: " + + repr(o)) + + return text + + self.encode_float = floatstr + _iterencode = _make_iterencode( - markers, self.default, _encoder, self.indent, floatstr, + markers, self.default, _encoder, self.indent, self.encode_float, self.key_separator, self.item_separator, self.sort_keys, self.skipkeys, _one_shot) return _iterencode(o, 0) + def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, ## HACK: hand-optimized bytecode; turn globals into locals diff --git a/Lib/test/test_json/test_dump.py b/Lib/test/test_json/test_dump.py index 13b40020781bae..819a5f5bf0c251 100644 --- a/Lib/test/test_json/test_dump.py +++ b/Lib/test/test_json/test_dump.py @@ -57,6 +57,21 @@ def __lt__(self, o): d[1337] = "true.dat" self.assertEqual(self.dumps(d, sort_keys=True), '{"1337": "true.dat"}') + # Issue 36841 + def test_encode_float(self): + data = {0.88: 0.9, 0: 0.1} + expected = '{"0.88": 0.9, "0": 0.1}' + self.assertEqual( + self.dumps(data, encode_float=None), + expected + ) + + data = {0.88: 0.9, 0: 0.1} + expected = '{"1": 1, "0": 0}' + self.assertEqual( + self.dumps(data, encode_float=lambda x: str(round(x))), + expected + ) class TestPyDump(TestDump, PyTest): pass diff --git a/Lib/test/test_json/test_speedups.py b/Lib/test/test_json/test_speedups.py index 682014cfd5b344..a6de50a30e9067 100644 --- a/Lib/test/test_json/test_speedups.py +++ b/Lib/test/test_json/test_speedups.py @@ -44,7 +44,7 @@ def test_bad_str_encoder(self): def bad_encoder1(*args): return None enc = self.json.encoder.c_make_encoder(None, lambda obj: str(obj), - bad_encoder1, None, ': ', ', ', + bad_encoder1, None, None, ': ', ', ', False, False, False) with self.assertRaises(TypeError): enc('spam', 4) @@ -54,7 +54,7 @@ def bad_encoder1(*args): def bad_encoder2(*args): 1/0 enc = self.json.encoder.c_make_encoder(None, lambda obj: str(obj), - bad_encoder2, None, ': ', ', ', + bad_encoder2, None, None, ': ', ', ', False, False, False) with self.assertRaises(ZeroDivisionError): enc('spam', 4) diff --git a/Misc/NEWS.d/next/Library/2019-05-10-21-55-45.bpo-36841.Dmzh-n.rst b/Misc/NEWS.d/next/Library/2019-05-10-21-55-45.bpo-36841.Dmzh-n.rst new file mode 100644 index 00000000000000..14a2a91dec6d5f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-05-10-21-55-45.bpo-36841.Dmzh-n.rst @@ -0,0 +1,2 @@ +Add encode_float argument to :class:`JSONEncoder` which enables customized +float encoding behavior diff --git a/Modules/_json.c b/Modules/_json.c index 360fb453cd111c..4461a9e2766750 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -43,6 +43,7 @@ typedef struct _PyEncoderObject { PyObject *defaultfn; PyObject *encoder; PyObject *indent; + PyObject *encode_float; PyObject *key_separator; PyObject *item_separator; char sort_keys; @@ -56,6 +57,7 @@ static PyMemberDef encoder_members[] = { {"default", T_OBJECT, offsetof(PyEncoderObject, defaultfn), READONLY, "default"}, {"encoder", T_OBJECT, offsetof(PyEncoderObject, encoder), READONLY, "encoder"}, {"indent", T_OBJECT, offsetof(PyEncoderObject, indent), READONLY, "indent"}, + {"encode_float", T_OBJECT, offsetof(PyEncoderObject, encode_float), READONLY, "encode_float"}, {"key_separator", T_OBJECT, offsetof(PyEncoderObject, key_separator), READONLY, "key_separator"}, {"item_separator", T_OBJECT, offsetof(PyEncoderObject, item_separator), READONLY, "item_separator"}, {"sort_keys", T_BOOL, offsetof(PyEncoderObject, sort_keys), READONLY, "sort_keys"}, @@ -1196,15 +1198,14 @@ static PyType_Spec PyScannerType_spec = { static PyObject * encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"markers", "default", "encoder", "indent", "key_separator", "item_separator", "sort_keys", "skipkeys", "allow_nan", NULL}; + static char *kwlist[] = {"markers", "default", "encoder", "indent", "encode_float", "key_separator", "item_separator", "sort_keys", "skipkeys", "allow_nan", NULL}; PyEncoderObject *s; - PyObject *markers, *defaultfn, *encoder, *indent, *key_separator; + PyObject *markers, *defaultfn, *encoder, *indent, *encode_float, *key_separator; PyObject *item_separator; int sort_keys, skipkeys, allow_nan; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOUUppp:make_encoder", kwlist, - &markers, &defaultfn, &encoder, &indent, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOOUUppp:make_encoder", kwlist, + &markers, &defaultfn, &encoder, &indent, &encode_float, &key_separator, &item_separator, &sort_keys, &skipkeys, &allow_nan)) return NULL; @@ -1224,6 +1225,7 @@ encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) s->defaultfn = Py_NewRef(defaultfn); s->encoder = Py_NewRef(encoder); s->indent = Py_NewRef(indent); + s->encode_float = Py_NewRef(encode_float); s->key_separator = Py_NewRef(key_separator); s->item_separator = Py_NewRef(item_separator); s->sort_keys = sort_keys; @@ -1296,6 +1298,9 @@ encoder_encode_float(PyEncoderObject *s, PyObject *obj) { /* Return the JSON representation of a PyFloat. */ double i = PyFloat_AS_DOUBLE(obj); + if (s->encode_float != Py_None) { + return PyObject_CallFunctionObjArgs(s->encode_float, obj, NULL); + } if (!Py_IS_FINITE(i)) { if (!s->allow_nan) { PyErr_Format( @@ -1694,6 +1699,7 @@ encoder_traverse(PyEncoderObject *self, visitproc visit, void *arg) Py_VISIT(self->defaultfn); Py_VISIT(self->encoder); Py_VISIT(self->indent); + Py_VISIT(self->encode_float); Py_VISIT(self->key_separator); Py_VISIT(self->item_separator); return 0; @@ -1707,6 +1713,7 @@ encoder_clear(PyEncoderObject *self) Py_CLEAR(self->defaultfn); Py_CLEAR(self->encoder); Py_CLEAR(self->indent); + Py_CLEAR(self->encode_float); Py_CLEAR(self->key_separator); Py_CLEAR(self->item_separator); return 0;