From c22c354f44f76f922da1308839a52cca07df73a0 Mon Sep 17 00:00:00 2001 From: Alex Rubinsteyn Date: Tue, 6 Oct 2026 12:35:09 -0400 Subject: [PATCH] Use the stdlib json module instead of simplejson; bump to 1.3.0 simplejson was a Python 2 era backport of json (see #10). The stdlib encoder is faster and produces byte-identical output for everything serializable emits (checked against golden payloads, including numpy float64 / IntEnum / float-Enum values, -0.0, denormals, huge ints and non-ASCII strings). NaN handling is now explicit instead of depending on the installed simplejson version (>=3.19 rejects NaN by default, older versions accept it): - to_json raises ValueError for NaN/inf (allow_nan=False), matching simplejson>=3.19, so existing output and content hashes don't change. - from_json accepts NaN/Infinity, so files written with older simplejson versions can be read again. Drops the simplejson dependency. vaxrank imports simplejson without declaring it, so openvax/vaxrank#580 should be released first. Supersedes PR #10. Part of #15 (the typechecks and coveralls dependencies are left for a separate change). Claude-Session: https://claude.ai/code/session_01JRDMg3F5kNKmnjv5jvQBgr --- README.md | 3 +++ docs/index.md | 4 ++++ pyproject.toml | 1 - serializable/helpers.py | 11 ++++++++--- serializable/version.py | 2 +- tests/test_primitive_values.py | 20 +++++++++++++++++++- 6 files changed, 35 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c033ecf..f2f9856 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,9 @@ If you change the keyword arguments to a class which derives from `Serializable` The format records Python classes and modules, which must remain importable when loading data. Keys starting with two underscores are reserved. Nested Python values require the serialization helpers rather than ordinary JSON encoding. +`to_json` raises `ValueError` for NaN or infinite floats, which standard JSON +can't represent; `from_json` still reads the `NaN` / `Infinity` literals which +older versions wrote. ## Documentation diff --git a/docs/index.md b/docs/index.md index b12ed81..89c1654 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,3 +66,7 @@ and objects implementing the serialization interface. Dictionary keys beginning with two underscores are reserved by the format. `to_dict()` is the object's field mapping; it does not by itself guarantee an ordinary JSON-compatible mapping when those fields contain nested Python objects. + +Floats must be finite: `to_json()` raises `ValueError` for NaN or infinity, +which standard JSON can't represent. `from_json()` still reads the `NaN` and +`Infinity` literals which older versions wrote. diff --git a/pyproject.toml b/pyproject.toml index 8f6053a..cb60156 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,6 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ "typechecks>=0.0.2", - "simplejson>=3.0.0", ] [project.optional-dependencies] diff --git a/serializable/helpers.py b/serializable/helpers.py index 5579417..5e63f18 100644 --- a/serializable/helpers.py +++ b/serializable/helpers.py @@ -17,12 +17,11 @@ from __future__ import annotations +import json from functools import lru_cache from types import BuiltinFunctionType, FunctionType from typing import Any, Union -import simplejson as json # type: ignore[import-untyped] - from .primitive_types import PRIMITIVE_TYPES @@ -310,13 +309,19 @@ def to_json(x: Any) -> str: """ Returns JSON representation of a given Serializable instance or other primitive object. + + Raises ValueError for NaN or infinite floats, which have no standard + JSON representation. """ - return json.dumps(to_serializable_repr(x)) + return json.dumps(to_serializable_repr(x), allow_nan=False) def from_json(json_string: Union[str, bytes]) -> Any: """ Inverse of to_json. Objects are reconstructed bottom-up while parsing, equivalent to (but faster than) from_serializable_repr(json.loads(...)). + + Accepts the NaN and Infinity literals which older versions of + serializable wrote. """ return json.loads(json_string, object_hook=_from_reconstructed_dict) diff --git a/serializable/version.py b/serializable/version.py index 1993484..06549ac 100644 --- a/serializable/version.py +++ b/serializable/version.py @@ -1,4 +1,4 @@ -__version__ = "1.2.1" +__version__ = "1.3.0" def print_version(): diff --git a/tests/test_primitive_values.py b/tests/test_primitive_values.py index f44ce61..99db282 100644 --- a/tests/test_primitive_values.py +++ b/tests/test_primitive_values.py @@ -10,7 +10,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from serializable import from_serializable_repr, to_serializable_repr +import math + +import pytest + +from serializable import from_json, from_serializable_repr, to_json, to_serializable_repr from .common import eq_ @@ -29,3 +33,17 @@ def test_bool(): def test_str(): eq_("waffles", from_serializable_repr(to_serializable_repr("waffles"))) + + +@pytest.mark.parametrize("value", [math.nan, math.inf, -math.inf]) +def test_to_json_rejects_non_finite_floats(value): + # NaN and infinities have no standard JSON representation + with pytest.raises(ValueError, match="Out of range float values"): + to_json([value]) + + +def test_from_json_accepts_non_finite_literals(): + # serializable used to write these (with simplejson < 3.19) + values = from_json("[NaN, Infinity, -Infinity]") + assert math.isnan(values[0]) + eq_(values[1:], [math.inf, -math.inf])