Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions Lib/json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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.
Expand All @@ -165,15 +169,16 @@ 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:
cls = JSONEncoder
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:
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -227,15 +235,15 @@ 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
return 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).encode(obj)
encode_float=encode_float, **kw).encode(obj)


_default_decoder = JSONDecoder(object_hook=None, object_pairs_hook=None)
Expand Down
59 changes: 34 additions & 25 deletions Lib/json/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comment: why move the floatstr function definition inline here? it could stay as a regular method in the class, then the line below would be self.encode_float = self.floatstr and still work.


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
Expand Down
15 changes: 15 additions & 0 deletions Lib/test/test_json/test_dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_json/test_speedups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add encode_float argument to :class:`JSONEncoder` which enables customized
float encoding behavior
17 changes: 12 additions & 5 deletions Modules/_json.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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"},
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down