From 326cee5eeba82a46e0f006d3945c9063ecef317a Mon Sep 17 00:00:00 2001 From: Rahul Date: Mon, 2 Nov 2026 15:29:22 +0530 Subject: [PATCH 1/4] fix: keep a leading + sign in the number spelling The JSON5 num_literal rule consumed a leading + but returned the unsigned token alone, while - was kept, so parse_int, parse_float and parse_constant received a spelling that differed from the document: loads('+NaN', parse_constant=str) gave 'NaN' and loads('+0xff', parse_int=str) gave '0xff'. Keep the + in the token, mirror the generated parser, and extend the two sign checks in lib.py (the hex-prefix test in _walk_ast and _parse_int) so +0xff parses as 255 instead of falling through to base 10. Values are unchanged; only hook spellings become as-written. Fixes #159 Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> --- json5/json5.g | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/json5/json5.g b/json5/json5.g index 7cafff3..2e00636 100644 --- a/json5/json5.g +++ b/json5/json5.g @@ -118,7 +118,7 @@ id_esc_continue = id_esc_start | ~~('u' ('200C' | '200c' | '200D' | '200d')) unicode_esc num_literal = '-' unsigned_lit:n -> '-' + n - | '+' unsigned_lit:n -> n + | '+' unsigned_lit:n -> '+' + n | unsigned_lit:n -> n unsigned_lit = dec_literal:d ~id_start -> d From 0bc8a79ad3da4d86932129b65b3dbf72f6e89560 Mon Sep 17 00:00:00 2001 From: Rahul Date: Mon, 2 Nov 2026 15:29:25 +0530 Subject: [PATCH 2/4] fix: keep a leading + sign in the number spelling Regenerated-parser equivalent of the json5.g change: the + branch of num_literal now returns the sign with the token, as the - branch does. Part of #159 Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> --- json5/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/json5/parser.py b/json5/parser.py index ab5deb7..dd6275f 100644 --- a/json5/parser.py +++ b/json5/parser.py @@ -1423,7 +1423,7 @@ def _num_literal__c1_(self): [ lambda: self._ch('+'), lambda: self._bind(self._unsigned_lit_, 'n'), - lambda: self._succeed(self._get('n')), + lambda: self._succeed('+' + self._get('n')), ] ) self._pop('num_literal__c1') From 5e3b7c66ca78120b0ae93abdd4b035fe549753a3 Mon Sep 17 00:00:00 2001 From: Rahul Date: Mon, 2 Nov 2026 15:29:28 +0530 Subject: [PATCH 3/4] fix: keep a leading + sign in the number spelling _walk_ast and the default _parse_int helper only stripped - before the hex-prefix test; a +0xff token would fall through to base-10 int() and raise. Strip either sign, so the + forms of hex literals parse the same way the - forms do. Part of #159 Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> --- json5/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/json5/lib.py b/json5/lib.py index 91c2b46..9c76355 100644 --- a/json5/lib.py +++ b/json5/lib.py @@ -372,7 +372,7 @@ def _fp_constant_parser(s): return float(s.replace('Infinity', 'inf').replace('NaN', 'nan')) def _parse_int(s): - if s.lstrip('-').startswith(('0x', '0X')): + if s.lstrip('+-').startswith(('0x', '0X')): return int(s, 16) return int(s) @@ -414,7 +414,7 @@ def _walk_ast( return False ty, v = el if ty == 'number': - unsigned = v[1:] if v.startswith('-') else v + unsigned = v[1:] if v[:1] in ('-', '+') else v if unsigned.startswith(('0x', '0X')): return parse_int(v) if '.' in v or 'e' in v or 'E' in v: From 2d43b78b7f3787619c2b8b57330c4303ee57b219 Mon Sep 17 00:00:00 2001 From: Rahul Date: Mon, 2 Nov 2026 15:29:30 +0530 Subject: [PATCH 4/4] test: parse hooks receive the as-written + spellings parse_int/parse_float/parse_constant now see '+5', '+0xff', '+1.5', '+Infinity' and '+NaN' with the sign, matching the - spellings that were already asserted. Part of #159 Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> --- tests/lib_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/lib_test.py b/tests/lib_test.py index 161928a..6379510 100644 --- a/tests/lib_test.py +++ b/tests/lib_test.py @@ -324,12 +324,17 @@ def hook(x): json5.loads('-Infinity', parse_constant=hook), '-Infinity' ) self.assertEqual(json5.loads('NaN', parse_constant=hook), 'NaN') + self.assertEqual( + json5.loads('+Infinity', parse_constant=hook), '+Infinity' + ) + self.assertEqual(json5.loads('+NaN', parse_constant=hook), '+NaN') def test_parse_float(self): def hook(x): return x self.assertEqual(json5.loads('1.0', parse_float=hook), '1.0') + self.assertEqual(json5.loads('+1.5', parse_float=hook), '+1.5') for text in ('1E5', '1E+5', '2.5E-3', '1e5', '1e-5'): with self.subTest(text=text): self.assertEqual(json5.loads(text, parse_float=hook), text) @@ -341,8 +346,11 @@ def hook(x): self.assertEqual(json5.loads('1', parse_int=hook), '1') self.assertEqual(json5.loads('0x10', parse_int=hook), '0x10') self.assertEqual(json5.loads('-0xff', parse_int=hook), '-0xff') + self.assertEqual(json5.loads('+5', parse_int=hook), '+5') + self.assertEqual(json5.loads('+0xff', parse_int=hook), '+0xff') self.assertEqual(json5.loads('0x10'), 16) self.assertEqual(json5.loads('-0xff'), -255) + self.assertEqual(json5.loads('+0xff'), 255) self.assertEqual(json5.loads('0X2a'), 42) def test_sample_file(self):