Skip to content

Commit

Permalink
Fixed serialization for custom ints and floats.
Browse files Browse the repository at this point in the history
[GitHub issue #57](#57)
Fixed serialization for objects that subclass `int` or `float`:
Previously we would use the objects __str__ implementation, but
that might result in an illegal JSON5 value if the object had
customized __str__ to return something illegal. Instead,
we follow the lead of the `JSON` module and call `int.__repr__`
or `float.__repr__` directly.

While I was at it, I added tests for dumps(-inf) and dumps(nan)
when those were supposed to be disallowed by `allow_nan=False`.
  • Loading branch information
dpranke committed Aug 1, 2022
1 parent 4b8217b commit 1f7f806
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 20 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ To run the tests, setup a venv and install the required dependencies with

## Version History / Release Notes

* v0.9.9 (2022-08-01)
* [GitHub issue #57](https://github.com/dpranke/pyjson5/issues/57)
Fixed serialization for objects that subclass `int` or `float`:
Previously we would use the objects __str__ implementation, but
that might result in an illegal JSON5 value if the object had
customized __str__ to return something illegal. Instead,
we follow the lead of the `JSON` module and call `int.__repr__`
or `float.__repr__` directly.
* While I was at it, I added tests for dumps(-inf) and dumps(nan)
when those were supposed to be disallowed by `allow_nan=False`.
* v0.9.8 (2022-05-08)
* [GitHub issue #47](https://github.com/dpranke/pyjson5/issues/47)
Fixed error reporting in some cases due to how parsing was handling
Expand Down
41 changes: 24 additions & 17 deletions json5/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,15 +264,36 @@ def _dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, indent,
s = u'false'
elif obj is None:
s = u'null'
elif obj == math.inf:
if allow_nan:
s = u'Infinity'
else:
raise ValueError()
elif obj == -math.inf:
if allow_nan:
s = u'-Infinity'
else:
raise ValueError()
elif isinstance(obj, float) and math.isnan(obj):
if allow_nan:
s = u'NaN'
else:
raise ValueError()
elif isinstance(obj, str_types):
if (is_key and _is_ident(obj) and not quote_keys
and not _is_reserved_word(obj)):
return True, obj
return True, _dump_str(obj, ensure_ascii)
elif isinstance(obj, float):
s = _dump_float(obj,allow_nan)
elif isinstance(obj, int):
s = str(obj)
# Subclasses of `int` and `float` may have custom
# __repr__ or __str__ methods, but the `JSON` library
# ignores them in order to ensure that the representation
# are just bare numbers. In order to match JSON's behavior
# we call the methods of the `float` and `int` class directly.
s = int.__repr__(obj)
elif isinstance(obj, float):
# See comment above for int
s = float.__repr__(obj)
else:
s = None

Expand Down Expand Up @@ -403,20 +424,6 @@ def _dump_array(obj, skipkeys, ensure_ascii, check_circular, allow_nan,
end_str + u']')


def _dump_float(obj, allow_nan):
if allow_nan:
if math.isnan(obj):
return 'NaN'
if obj == float('inf'):
return 'Infinity'
if obj == float('-inf'):
return '-Infinity'
elif math.isnan(obj) or obj == float('inf') or obj == float('-inf'):
raise ValueError('Out of range float values '
'are not JSON compliant')
return str(obj)


def _dump_str(obj, ensure_ascii):
ret = ['"']
for ch in obj:
Expand Down
2 changes: 1 addition & 1 deletion json5/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.

VERSION = '0.9.8'
VERSION = '0.9.9'
15 changes: 13 additions & 2 deletions tests/lib_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,13 +347,20 @@ def __len__(self):
self.assertEqual(json5.dumps(MyArray()), '[0, 1, 1]')

def test_custom_numbers(self):
# See https://github.com/dpranke/pyjson5/issues/57: we
# need to ensure that we use the bare int.__repr__ and
# float.__repr__ in order to get legal JSON values when
# people have custom subclasses with customer __repr__ methods.
# (This is what JSON does and we want to match it).
class MyInt(int):
pass
def __repr__(self):
return 'fail'

self.assertEqual(json5.dumps(MyInt(5)), '5')

class MyFloat(float):
pass
def __repr__(self):
return 'fail'

self.assertEqual(json5.dumps(MyFloat(0.5)), '0.5')

Expand Down Expand Up @@ -427,6 +434,10 @@ def test_numbers(self):

self.assertRaises(ValueError, json5.dumps,
float('inf'), allow_nan=False)
self.assertRaises(ValueError, json5.dumps,
float('-inf'), allow_nan=False)
self.assertRaises(ValueError, json5.dumps,
float('nan'), allow_nan=False)

def test_null(self):
self.check(None, 'null')
Expand Down

0 comments on commit 1f7f806

Please sign in to comment.