diff --git a/README.md b/README.md index 7c54fa7..ec846d1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/json5/lib.py b/json5/lib.py index e56e825..6b3056e 100644 --- a/json5/lib.py +++ b/json5/lib.py @@ -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 @@ -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: diff --git a/json5/version.py b/json5/version.py index ed8c2f6..0a39014 100644 --- a/json5/version.py +++ b/json5/version.py @@ -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' diff --git a/tests/lib_test.py b/tests/lib_test.py index a6d2473..68842e7 100644 --- a/tests/lib_test.py +++ b/tests/lib_test.py @@ -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') @@ -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')