simplejson 4.1.1__tar.gz → 4.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {simplejson-4.1.1 → simplejson-4.1.2}/CHANGES.txt +12 -0
  2. {simplejson-4.1.1/simplejson.egg-info → simplejson-4.1.2}/PKG-INFO +1 -1
  3. {simplejson-4.1.1 → simplejson-4.1.2}/conf.py +2 -2
  4. {simplejson-4.1.1 → simplejson-4.1.2}/setup.py +1 -1
  5. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/__init__.py +1 -1
  6. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/_speedups.c +88 -26
  7. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/_speedups_scan.h +6 -2
  8. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/decoder.py +5 -3
  9. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/encoder.py +33 -4
  10. simplejson-4.1.2/simplejson/tests/test_decimal.py +133 -0
  11. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_decode.py +27 -0
  12. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_fail.py +2 -0
  13. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_scanstring.py +46 -0
  14. {simplejson-4.1.1 → simplejson-4.1.2/simplejson.egg-info}/PKG-INFO +1 -1
  15. simplejson-4.1.1/simplejson/tests/test_decimal.py +0 -71
  16. {simplejson-4.1.1 → simplejson-4.1.2}/LICENSE.txt +0 -0
  17. {simplejson-4.1.1 → simplejson-4.1.2}/MANIFEST.in +0 -0
  18. {simplejson-4.1.1 → simplejson-4.1.2}/README.rst +0 -0
  19. {simplejson-4.1.1 → simplejson-4.1.2}/index.rst +0 -0
  20. {simplejson-4.1.1 → simplejson-4.1.2}/pyproject.toml +0 -0
  21. {simplejson-4.1.1 → simplejson-4.1.2}/scripts/make_docs.py +0 -0
  22. {simplejson-4.1.1 → simplejson-4.1.2}/setup.cfg +0 -0
  23. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/compat.py +0 -0
  24. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/errors.py +0 -0
  25. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/ordered_dict.py +0 -0
  26. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/raw_json.py +0 -0
  27. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/scanner.py +0 -0
  28. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/__init__.py +0 -0
  29. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/_cibw_runner.py +0 -0
  30. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/_helpers.py +0 -0
  31. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_bigint_as_string.py +0 -0
  32. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_bitsize_int_as_string.py +0 -0
  33. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_check_circular.py +0 -0
  34. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_default.py +0 -0
  35. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_dump.py +0 -0
  36. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_encode_basestring_ascii.py +0 -0
  37. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_encode_for_html.py +0 -0
  38. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_errors.py +0 -0
  39. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_float.py +0 -0
  40. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_for_json.py +0 -0
  41. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_free_threading.py +0 -0
  42. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_indent.py +0 -0
  43. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_item_sort_key.py +0 -0
  44. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_iterable.py +0 -0
  45. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_namedtuple.py +0 -0
  46. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_pass1.py +0 -0
  47. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_pass2.py +0 -0
  48. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_pass3.py +0 -0
  49. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_raw_json.py +0 -0
  50. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_recursion.py +0 -0
  51. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_separators.py +0 -0
  52. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_speedups.py +0 -0
  53. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_str_subclass.py +0 -0
  54. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_subclass.py +0 -0
  55. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_subinterpreters.py +0 -0
  56. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_tool.py +0 -0
  57. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_tuple.py +0 -0
  58. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tests/test_unicode.py +0 -0
  59. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson/tool.py +0 -0
  60. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson.egg-info/SOURCES.txt +0 -0
  61. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson.egg-info/dependency_links.txt +0 -0
  62. {simplejson-4.1.1 → simplejson-4.1.2}/simplejson.egg-info/top_level.txt +0 -0
  63. {simplejson-4.1.1 → simplejson-4.1.2}/tsan_stress_simplejson.py +0 -0
@@ -1,3 +1,15 @@
1
+ Version 4.1.2 released 2026-08-26
2
+
3
+ * Fix control character error position when content precedes the control char
4
+ https://github.com/simplejson/simplejson/pull/379
5
+ * Report the offending char, not the backslash, for invalid \X escapes
6
+ https://github.com/simplejson/simplejson/pull/380
7
+ * Handle non-finite Decimals like floats in the encoder
8
+ https://github.com/simplejson/simplejson/pull/381
9
+ * Parse failures due to trailing commas are now reported consistently
10
+ across implementations
11
+ https://github.com/simplejson/simplejson/pull/382
12
+
1
13
  Version 4.1.1 released 2026-04-24
2
14
 
3
15
  * The ``build_wheels_py27`` CI job now also builds Python 2.7 wheels
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simplejson
3
- Version: 4.1.1
3
+ Version: 4.1.2
4
4
  Summary: Simple, fast, extensible JSON encoder/decoder for Python
5
5
  Home-page: https://github.com/simplejson/simplejson
6
6
  Author: Bob Ippolito
@@ -36,7 +36,7 @@ master_doc = 'index'
36
36
 
37
37
  # General substitutions.
38
38
  project = 'simplejson'
39
- copyright = '2025, Bob Ippolito'
39
+ copyright = '2026, Bob Ippolito'
40
40
 
41
41
  # The default replacements for |version| and |release|, also used in various
42
42
  # other places throughout the built documents.
@@ -44,7 +44,7 @@ copyright = '2025, Bob Ippolito'
44
44
  # The short X.Y version.
45
45
  version = '4.1'
46
46
  # The full version, including alpha/beta/rc tags.
47
- release = '4.1.1'
47
+ release = '4.1.2'
48
48
 
49
49
  # There are two options for replacing |today|: either, you set today to some
50
50
  # non-false value, then it is used:
@@ -12,7 +12,7 @@ from distutils.errors import CCompilerError, DistutilsExecError, \
12
12
 
13
13
  IS_PYPY = hasattr(sys, 'pypy_translation_info')
14
14
  IS_GRAALPY = getattr(getattr(sys, "implementation", None), "name", None) == "graalpy"
15
- VERSION = '4.1.1'
15
+ VERSION = '4.1.2'
16
16
  DESCRIPTION = "Simple, fast, extensible JSON encoder/decoder for Python"
17
17
 
18
18
  with open('README.rst', 'r') as f:
@@ -118,7 +118,7 @@ Serializing multiple objects to JSON lines (newline-delimited JSON)::
118
118
 
119
119
  """
120
120
  from __future__ import absolute_import
121
- __version__ = '4.1.1'
121
+ __version__ = '4.1.2'
122
122
  __all__ = [
123
123
  'dump', 'dumps', 'load', 'loads',
124
124
  'JSONDecoder', 'JSONDecodeError', 'JSONEncoder',
@@ -429,6 +429,8 @@ static PyObject *
429
429
  encoder_long_to_str(PyObject *obj);
430
430
  static PyObject *
431
431
  encoder_encode_float(PyEncoderObject *s, PyObject *obj);
432
+ static PyObject *
433
+ encoder_encode_decimal(PyEncoderObject *s, PyObject *obj);
432
434
  static int
433
435
  encoder_accumulate_newline_indent(PyEncoderObject *s, _speedups_state *state,
434
436
  JSON_Accu *rval, Py_ssize_t indent_level);
@@ -1062,7 +1064,7 @@ encoder_stringify_key(PyEncoderObject *s, PyObject *key)
1062
1064
  return encoder_long_to_str(key);
1063
1065
  }
1064
1066
  else if (s->use_decimal && PyObject_TypeCheck(key, (PyTypeObject *)s->Decimal)) {
1065
- return PyObject_Str(key);
1067
+ return encoder_encode_decimal(s, key);
1066
1068
  }
1067
1069
  if (s->skipkeys) {
1068
1070
  Py_INCREF(Py_None);
@@ -1365,7 +1367,7 @@ scanstring_str(_speedups_state *state, PyObject *pystr, Py_ssize_t end,
1365
1367
  default: c = 0;
1366
1368
  }
1367
1369
  if (c == 0) {
1368
- raise_errmsg(state, ERR_STRING_ESC1, pystr, end - 2);
1370
+ raise_errmsg(state, ERR_STRING_ESC1, pystr, end - 1);
1369
1371
  goto bail;
1370
1372
  }
1371
1373
  }
@@ -1564,7 +1566,7 @@ scanstring_unicode(_speedups_state *state, PyObject *pystr, Py_ssize_t end,
1564
1566
  default: c = 0;
1565
1567
  }
1566
1568
  if (c == 0) {
1567
- raise_errmsg(state, ERR_STRING_ESC1, pystr, end - 2);
1569
+ raise_errmsg(state, ERR_STRING_ESC1, pystr, end - 1);
1568
1570
  goto bail;
1569
1571
  }
1570
1572
  }
@@ -1755,7 +1757,7 @@ scanstring_unicode(_speedups_state *state, PyObject *pystr, Py_ssize_t end,
1755
1757
  default: c = 0;
1756
1758
  }
1757
1759
  if (c == 0) {
1758
- raise_errmsg(state, ERR_STRING_ESC1, pystr, end - 2);
1760
+ raise_errmsg(state, ERR_STRING_ESC1, pystr, end - 1);
1759
1761
  goto bail;
1760
1762
  }
1761
1763
  }
@@ -2785,33 +2787,38 @@ _encoded_const(_speedups_state *state, PyObject *obj)
2785
2787
  }
2786
2788
  }
2787
2789
 
2790
+ static PyObject *
2791
+ encoder_encode_nonfinite(PyEncoderObject *s, int sign)
2792
+ {
2793
+ _speedups_state *state = get_speedups_state(s->module_ref);
2794
+
2795
+ if (!s->allow_or_ignore_nan) {
2796
+ PyErr_SetString(PyExc_ValueError, "Out of range float values are not JSON compliant");
2797
+ return NULL;
2798
+ }
2799
+ if (s->allow_or_ignore_nan & JSON_IGNORE_NAN) {
2800
+ return _encoded_const(state, Py_None);
2801
+ }
2802
+ /* JSON_ALLOW_NAN is set */
2803
+ if (sign > 0) {
2804
+ Py_INCREF(state->JSON_Infinity);
2805
+ return state->JSON_Infinity;
2806
+ }
2807
+ if (sign < 0) {
2808
+ Py_INCREF(state->JSON_NegInfinity);
2809
+ return state->JSON_NegInfinity;
2810
+ }
2811
+ Py_INCREF(state->JSON_NaN);
2812
+ return state->JSON_NaN;
2813
+ }
2814
+
2788
2815
  static PyObject *
2789
2816
  encoder_encode_float(PyEncoderObject *s, PyObject *obj)
2790
2817
  {
2791
2818
  /* Return the JSON representation of a PyFloat */
2792
- _speedups_state *state = get_speedups_state(s->module_ref);
2793
2819
  double i = PyFloat_AS_DOUBLE(obj);
2794
2820
  if (!Py_IS_FINITE(i)) {
2795
- if (!s->allow_or_ignore_nan) {
2796
- PyErr_SetString(PyExc_ValueError, "Out of range float values are not JSON compliant");
2797
- return NULL;
2798
- }
2799
- if (s->allow_or_ignore_nan & JSON_IGNORE_NAN) {
2800
- return _encoded_const(state, Py_None);
2801
- }
2802
- /* JSON_ALLOW_NAN is set */
2803
- else if (i > 0) {
2804
- Py_INCREF(state->JSON_Infinity);
2805
- return state->JSON_Infinity;
2806
- }
2807
- else if (i < 0) {
2808
- Py_INCREF(state->JSON_NegInfinity);
2809
- return state->JSON_NegInfinity;
2810
- }
2811
- else {
2812
- Py_INCREF(state->JSON_NaN);
2813
- return state->JSON_NaN;
2814
- }
2821
+ return encoder_encode_nonfinite(s, i > 0 ? 1 : (i < 0 ? -1 : 0));
2815
2822
  }
2816
2823
  /* Use a better float format here? */
2817
2824
  if (PyFloat_CheckExact(obj)) {
@@ -2830,6 +2837,61 @@ encoder_encode_float(PyEncoderObject *s, PyObject *obj)
2830
2837
  }
2831
2838
  }
2832
2839
 
2840
+ static PyObject *
2841
+ encoder_encode_decimal(PyEncoderObject *s, PyObject *obj)
2842
+ {
2843
+ /* Return the JSON representation of a Decimal. Finite Decimals are
2844
+ stringified as-is (preserving precision and trailing zeros); non-finite
2845
+ Decimals (NaN, sNaN, Infinity) are routed through the same
2846
+ allow_nan/ignore_nan handling as floats so they never emit invalid
2847
+ JSON. See https://github.com/simplejson/simplejson/issues/149
2848
+
2849
+ str(Decimal) carries a leading '-' sign at most (never '+'); a finite
2850
+ value's first significant character is always a digit while a non-finite
2851
+ value's is a letter ('N'/'s' for [s]NaN, 'I' for Infinity). So a single
2852
+ str() plus a one/two character check distinguishes the two. */
2853
+ PyObject *str = PyObject_Str(obj);
2854
+ Py_ssize_t len;
2855
+ int negative;
2856
+ JSON_UNICHR c;
2857
+ #if PY_MAJOR_VERSION >= 3
2858
+ int kind;
2859
+ void *data;
2860
+ #else
2861
+ const char *data;
2862
+ #endif
2863
+ if (str == NULL)
2864
+ return NULL;
2865
+ #if PY_MAJOR_VERSION >= 3
2866
+ if (PyUnicode_READY(str)) {
2867
+ Py_DECREF(str);
2868
+ return NULL;
2869
+ }
2870
+ len = PyUnicode_GET_LENGTH(str);
2871
+ kind = PyUnicode_KIND(str);
2872
+ data = PyUnicode_DATA(str);
2873
+ negative = (len > 0 && PyUnicode_READ(kind, data, 0) == '-');
2874
+ c = (JSON_UNICHR)0;
2875
+ if (len > (negative ? 1 : 0))
2876
+ c = PyUnicode_READ(kind, data, negative ? 1 : 0);
2877
+ #else
2878
+ len = PyString_GET_SIZE(str);
2879
+ data = PyString_AS_STRING(str);
2880
+ negative = (len > 0 && data[0] == '-');
2881
+ c = (JSON_UNICHR)0;
2882
+ if (len > (negative ? 1 : 0))
2883
+ c = (unsigned char)data[negative ? 1 : 0];
2884
+ #endif
2885
+ if (IS_DIGIT(c)) {
2886
+ /* First significant character is a digit: finite Decimal, emit as-is. */
2887
+ return str;
2888
+ }
2889
+ /* Non-finite Decimal; use the same allow_nan/ignore_nan path as floats
2890
+ without creating a temporary float. */
2891
+ Py_DECREF(str);
2892
+ return encoder_encode_nonfinite(s, c == 'I' ? (negative ? -1 : 1) : 0);
2893
+ }
2894
+
2833
2895
  static PyObject *
2834
2896
  encoder_encode_string(PyEncoderObject *s, PyObject *obj)
2835
2897
  {
@@ -3090,7 +3152,7 @@ encoder_listencode_obj(PyEncoderObject *s, JSON_Accu *rval, PyObject *obj, Py_ss
3090
3152
  Py_LeaveRecursiveCall();
3091
3153
  }
3092
3154
  else if (s->use_decimal && PyObject_TypeCheck(obj, (PyTypeObject *)s->Decimal)) {
3093
- PyObject *encoded = PyObject_Str(obj);
3155
+ PyObject *encoded = encoder_encode_decimal(s, obj);
3094
3156
  if (encoded != NULL)
3095
3157
  rv = _steal_accumulate(state, rval, encoded);
3096
3158
  }
@@ -204,6 +204,7 @@ JSON_SCAN_FN(_parse_object)(PyScannerObject *s, PyObject *pystr,
204
204
  /* only loop if the object is non-empty */
205
205
  if (idx <= end_idx && JSON_SCAN_READ(idx) != '}') {
206
206
  int trailing_delimiter = 0;
207
+ Py_ssize_t comma_idx = 0;
207
208
  while (idx <= end_idx) {
208
209
  trailing_delimiter = 0;
209
210
 
@@ -271,6 +272,7 @@ JSON_SCAN_FN(_parse_object)(PyScannerObject *s, PyObject *pystr,
271
272
  raise_errmsg(state, ERR_OBJECT_DELIMITER, pystr, idx);
272
273
  goto bail;
273
274
  }
275
+ comma_idx = idx;
274
276
  idx++;
275
277
 
276
278
  /* skip whitespace after , delimiter */
@@ -279,7 +281,7 @@ JSON_SCAN_FN(_parse_object)(PyScannerObject *s, PyObject *pystr,
279
281
 
280
282
  /* check for trailing comma before } */
281
283
  if (idx <= end_idx && JSON_SCAN_READ(idx) == '}') {
282
- raise_errmsg(state, ERR_TRAILING_COMMA_OBJECT, pystr, idx);
284
+ raise_errmsg(state, ERR_TRAILING_COMMA_OBJECT, pystr, comma_idx);
283
285
  goto bail;
284
286
  }
285
287
  }
@@ -351,6 +353,7 @@ JSON_SCAN_FN(_parse_array)(PyScannerObject *s, PyObject *pystr,
351
353
  /* only loop if the array is non-empty */
352
354
  if (idx <= end_idx && JSON_SCAN_READ(idx) != ']') {
353
355
  int trailing_delimiter = 0;
356
+ Py_ssize_t comma_idx = 0;
354
357
  while (idx <= end_idx) {
355
358
  trailing_delimiter = 0;
356
359
  /* read any JSON term and de-tuplefy the (rval, idx) */
@@ -377,6 +380,7 @@ JSON_SCAN_FN(_parse_array)(PyScannerObject *s, PyObject *pystr,
377
380
  raise_errmsg(state, ERR_ARRAY_DELIMITER, pystr, idx);
378
381
  goto bail;
379
382
  }
383
+ comma_idx = idx;
380
384
  idx++;
381
385
 
382
386
  /* skip whitespace after , */
@@ -385,7 +389,7 @@ JSON_SCAN_FN(_parse_array)(PyScannerObject *s, PyObject *pystr,
385
389
 
386
390
  /* check for trailing comma before ] */
387
391
  if (idx <= end_idx && JSON_SCAN_READ(idx) == ']') {
388
- raise_errmsg(state, ERR_TRAILING_COMMA_ARRAY, pystr, idx);
392
+ raise_errmsg(state, ERR_TRAILING_COMMA_ARRAY, pystr, comma_idx);
389
393
  goto bail;
390
394
  }
391
395
  }
@@ -102,7 +102,7 @@ def py_scanstring(s, end, encoding=None, strict=True,
102
102
  elif terminator != '\\':
103
103
  if strict:
104
104
  msg = "Invalid control character %r at"
105
- raise JSONDecodeError(msg, s, prev_end)
105
+ raise JSONDecodeError(msg, s, end - 1)
106
106
  else:
107
107
  _append(terminator)
108
108
  continue
@@ -213,6 +213,7 @@ def JSONObject(state, encoding, strict, scan_once, object_hook,
213
213
  break
214
214
  elif nextchar != ',':
215
215
  raise JSONDecodeError("Expecting ',' delimiter or '}'", s, end - 1)
216
+ comma_idx = end - 1
216
217
 
217
218
  try:
218
219
  nextchar = s[end]
@@ -230,7 +231,7 @@ def JSONObject(state, encoding, strict, scan_once, object_hook,
230
231
  if nextchar == '}':
231
232
  raise JSONDecodeError(
232
233
  "Illegal trailing comma before end of object",
233
- s, end - 1)
234
+ s, comma_idx)
234
235
  raise JSONDecodeError(
235
236
  "Expecting property name enclosed in double quotes",
236
237
  s, end - 1)
@@ -271,6 +272,7 @@ def JSONArray(state, scan_once, array_hook=None,
271
272
  break
272
273
  elif nextchar != ',':
273
274
  raise JSONDecodeError("Expecting ',' delimiter or ']'", s, end - 1)
275
+ comma_idx = end - 1
274
276
 
275
277
  try:
276
278
  if s[end] in _ws:
@@ -283,7 +285,7 @@ def JSONArray(state, scan_once, array_hook=None,
283
285
  if s[end:end + 1] == ']':
284
286
  raise JSONDecodeError(
285
287
  "Illegal trailing comma before end of array",
286
- s, end - 1)
288
+ s, comma_idx)
287
289
 
288
290
  if array_hook is not None:
289
291
  values = array_hook(values)
@@ -43,6 +43,35 @@ del i
43
43
 
44
44
  FLOAT_REPR = repr
45
45
 
46
+ _NAN = float('nan')
47
+ _INFINITY = float('inf')
48
+
49
+
50
+ def _encode_decimal(d, _floatstr):
51
+ """Return the JSON representation of a ``Decimal``.
52
+
53
+ Finite Decimals are stringified as-is, preserving precision and trailing
54
+ zeros. Non-finite Decimals (``NaN``, ``sNaN``, ``Infinity``) are routed
55
+ through the same ``allow_nan``/``ignore_nan`` handling as floats so they
56
+ never emit invalid JSON (see #149).
57
+
58
+ ``str(Decimal)`` yields a leading ``-`` sign at most (never ``+``); a
59
+ finite value's first significant character is always a digit, while a
60
+ non-finite value's is a letter (``N``, ``s`` or ``I``). So a single
61
+ ``str()`` plus a one/two character check distinguishes the two.
62
+ """
63
+ s = str(d)
64
+ c0 = s[:1]
65
+ c = s[1:2] if c0 == '-' else c0
66
+ if '0' <= c <= '9':
67
+ # First significant character is a digit: finite Decimal.
68
+ return s
69
+ # Non-finite Decimal. ``N``/``s`` -> NaN (float(Decimal('sNaN')) raises,
70
+ # so map both quiet and signaling NaN to a plain NaN); ``I`` -> Infinity.
71
+ if c == 'I':
72
+ return _floatstr(-_INFINITY if c0 == '-' else _INFINITY)
73
+ return _floatstr(_NAN)
74
+
46
75
  # dict-like types that should be encoded as JSON objects.
47
76
  # frozendict is a builtin added in CPython 3.15 (PEP 814).
48
77
  if sys.version_info >= (3, 15):
@@ -536,7 +565,7 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
536
565
  elif isinstance(value, float):
537
566
  yield buf + _floatstr(value)
538
567
  elif _use_decimal and isinstance(value, Decimal):
539
- yield buf + str(value)
568
+ yield buf + _encode_decimal(value, _floatstr)
540
569
  else:
541
570
  yield buf
542
571
  for_json = _for_json and call_method(value, 'for_json')
@@ -596,7 +625,7 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
596
625
  key = int(key)
597
626
  key = str(key)
598
627
  elif _use_decimal and isinstance(key, Decimal):
599
- key = str(key)
628
+ key = _encode_decimal(key, _floatstr)
600
629
  elif _skipkeys:
601
630
  key = None
602
631
  else:
@@ -668,7 +697,7 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
668
697
  elif isinstance(value, float):
669
698
  yield _floatstr(value)
670
699
  elif _use_decimal and isinstance(value, Decimal):
671
- yield str(value)
700
+ yield _encode_decimal(value, _floatstr)
672
701
  else:
673
702
  for_json = _for_json and call_method(value, 'for_json')
674
703
  if for_json:
@@ -744,7 +773,7 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
744
773
  for chunk in _iterencode_dict(o, _current_indent_level):
745
774
  yield chunk
746
775
  elif _use_decimal and isinstance(o, Decimal):
747
- yield str(o)
776
+ yield _encode_decimal(o, _floatstr)
748
777
  else:
749
778
  while _iterable_as_array:
750
779
  # Markers are not checked here because it is valid for
@@ -0,0 +1,133 @@
1
+ import decimal
2
+ from decimal import Decimal
3
+ from unittest import TestCase
4
+ from simplejson.compat import StringIO, reload_module
5
+
6
+ import simplejson as json
7
+
8
+ class TestDecimal(TestCase):
9
+ NUMS = "1.0", "10.00", "1.1", "1234567890.1234567890", "500"
10
+ def dumps(self, obj, **kw):
11
+ sio = StringIO()
12
+ json.dump(obj, sio, **kw)
13
+ res = json.dumps(obj, **kw)
14
+ self.assertEqual(res, sio.getvalue())
15
+ return res
16
+
17
+ def loads(self, s, **kw):
18
+ sio = StringIO(s)
19
+ res = json.loads(s, **kw)
20
+ self.assertEqual(res, json.load(sio, **kw))
21
+ return res
22
+
23
+ def test_decimal_encode(self):
24
+ for d in map(Decimal, self.NUMS):
25
+ self.assertEqual(self.dumps(d, use_decimal=True), str(d))
26
+
27
+ def test_decimal_decode(self):
28
+ for s in self.NUMS:
29
+ self.assertEqual(self.loads(s, parse_float=Decimal), Decimal(s))
30
+
31
+ def test_stringify_key(self):
32
+ for d in map(Decimal, self.NUMS):
33
+ v = {d: d}
34
+ self.assertEqual(
35
+ self.loads(
36
+ self.dumps(v, use_decimal=True), parse_float=Decimal),
37
+ {str(d): d})
38
+
39
+ def test_decimal_roundtrip(self):
40
+ for d in map(Decimal, self.NUMS):
41
+ # The type might not be the same (int and Decimal) but they
42
+ # should still compare equal.
43
+ for v in [d, [d], {'': d}]:
44
+ self.assertEqual(
45
+ self.loads(
46
+ self.dumps(v, use_decimal=True), parse_float=Decimal),
47
+ v)
48
+
49
+ def test_decimal_defaults(self):
50
+ d = Decimal('1.1')
51
+ # use_decimal=True is the default
52
+ self.assertRaises(TypeError, json.dumps, d, use_decimal=False)
53
+ self.assertEqual('1.1', json.dumps(d))
54
+ self.assertEqual('1.1', json.dumps(d, use_decimal=True))
55
+ self.assertRaises(TypeError, json.dump, d, StringIO(),
56
+ use_decimal=False)
57
+ sio = StringIO()
58
+ json.dump(d, sio)
59
+ self.assertEqual('1.1', sio.getvalue())
60
+ sio = StringIO()
61
+ json.dump(d, sio, use_decimal=True)
62
+ self.assertEqual('1.1', sio.getvalue())
63
+
64
+ def test_decimal_reload(self):
65
+ # Simulate a subinterpreter that reloads the Python modules but not
66
+ # the C code https://github.com/simplejson/simplejson/issues/34
67
+ global Decimal
68
+ Decimal = reload_module(decimal).Decimal
69
+ import simplejson.encoder
70
+ simplejson.encoder.Decimal = Decimal
71
+ self.test_decimal_roundtrip()
72
+
73
+ def test_decimal_nan_allow(self):
74
+ # Non-finite Decimals should be treated the same as the matching
75
+ # float, i.e. emit the JavaScript literals when allow_nan is true.
76
+ # https://github.com/simplejson/simplejson/issues/149
77
+ self.assertEqual(json.dumps(Decimal('NaN'), allow_nan=True), 'NaN')
78
+ self.assertEqual(
79
+ json.dumps(Decimal('Infinity'), allow_nan=True), 'Infinity')
80
+ self.assertEqual(
81
+ json.dumps(Decimal('-Infinity'), allow_nan=True), '-Infinity')
82
+ # sNaN is also not finite and must not leak through as a literal.
83
+ self.assertEqual(json.dumps(Decimal('sNaN'), allow_nan=True), 'NaN')
84
+
85
+ def test_decimal_nan_ignore(self):
86
+ # ignore_nan should emit null for non-finite Decimals, matching float.
87
+ # https://github.com/simplejson/simplejson/issues/149
88
+ for d in (Decimal('NaN'), Decimal('sNaN'),
89
+ Decimal('Infinity'), Decimal('-Infinity')):
90
+ self.assertEqual(json.dumps(d, ignore_nan=True), 'null')
91
+ self.assertEqual(
92
+ json.dumps([Decimal('0.33'), Decimal('NaN'), Decimal('0.20')],
93
+ ignore_nan=True),
94
+ '[0.33, null, 0.20]')
95
+
96
+ def test_decimal_nan_deny(self):
97
+ # allow_nan=False must raise for non-finite Decimals rather than
98
+ # silently emit invalid JSON.
99
+ # https://github.com/simplejson/simplejson/issues/149
100
+ for d in (Decimal('NaN'), Decimal('sNaN'),
101
+ Decimal('Infinity'), Decimal('-Infinity')):
102
+ self.assertRaises(ValueError, json.dumps, d, allow_nan=False)
103
+ # allow_nan defaults to False, so a plain dumps must raise too.
104
+ self.assertRaises(ValueError, json.dumps, Decimal('NaN'))
105
+
106
+ def test_decimal_nan_as_value(self):
107
+ # Non-finite Decimals nested inside containers must be handled too.
108
+ self.assertEqual(
109
+ json.dumps({'a': Decimal('NaN')}, allow_nan=True), '{"a": NaN}')
110
+ self.assertEqual(
111
+ json.dumps({'a': Decimal('Infinity')}, ignore_nan=True),
112
+ '{"a": null}')
113
+ self.assertRaises(
114
+ ValueError, json.dumps, [Decimal('NaN')], allow_nan=False)
115
+
116
+ def test_decimal_nan_as_key(self):
117
+ # A non-finite Decimal used as a dict key follows the same rules.
118
+ self.assertEqual(
119
+ json.dumps({Decimal('NaN'): 1}, allow_nan=True), '{"NaN": 1}')
120
+ self.assertEqual(
121
+ json.dumps({Decimal('Infinity'): 1}, ignore_nan=True),
122
+ '{"null": 1}')
123
+ self.assertRaises(
124
+ ValueError, json.dumps, {Decimal('NaN'): 1}, allow_nan=False)
125
+
126
+ def test_decimal_finite_unaffected(self):
127
+ # The fix must not perturb finite Decimals, including zero and
128
+ # values that would lose trailing zeros if routed through float().
129
+ for s in ('0', '0.00', '-0', '1.50', '1E-30', '1234567890.1234567890'):
130
+ d = Decimal(s)
131
+ self.assertEqual(json.dumps(d), str(d))
132
+ self.assertEqual(json.dumps(d, ignore_nan=True), str(d))
133
+ self.assertEqual(json.dumps(d, allow_nan=True), str(d))
@@ -165,6 +165,33 @@ class TestDecode(TestCase):
165
165
  self.assertIn(expected_msg, str(e),
166
166
  "Wrong error for %r: %s" % (doc, e))
167
167
 
168
+ def test_trailing_comma_position(self):
169
+ # The reported position is the comma itself, not the closing
170
+ # bracket and not whatever whitespace happens to precede it.
171
+ # Same positions as the stdlib json module. Runs on both the C
172
+ # and the pure-Python scanner.
173
+ test_cases = [
174
+ ('[42,]', 3),
175
+ ('[42, ]', 3),
176
+ ('[42,\n]', 3),
177
+ ('[123 , ]', 6),
178
+ ('{"spam":42,}', 10),
179
+ ('{"spam":42 , }', 11),
180
+ ('{"spam":42,\n}', 10),
181
+ ('[[1, 2],]', 7),
182
+ ('{"a": {"b": 1,},}', 13),
183
+ ]
184
+ for doc, expected_pos in test_cases:
185
+ try:
186
+ json.loads(doc)
187
+ self.fail("Expected JSONDecodeError for %r" % (doc,))
188
+ except json.JSONDecodeError as e:
189
+ self.assertEqual(doc[e.pos], ',',
190
+ "Position %d of %r is %r, not the comma"
191
+ % (e.pos, doc, doc[e.pos]))
192
+ self.assertEqual(e.pos, expected_pos,
193
+ "Wrong position for %r: %d" % (doc, e.pos))
194
+
168
195
  def test_nonascii_digits_rejected(self):
169
196
  # Non-ASCII digits (e.g. fullwidth digits) must not be accepted
170
197
  # as JSON numbers. Matches CPython gh-125687.
@@ -158,6 +158,8 @@ class TestFail(TestCase):
158
158
  ('[,', "Expecting value", 1),
159
159
  ('--', 'Expecting value', 0),
160
160
  ('"\x18d', "Invalid control character %r", 1),
161
+ # control char after content: pos must point to the control char, not start of content
162
+ ('"d\x18"', "Invalid control character %r", 2),
161
163
  ]
162
164
  for data, msg, idx in test_cases:
163
165
  try:
@@ -290,3 +290,49 @@ class TestScanString(TestCase):
290
290
  self.assertEqual(
291
291
  scanstring(u'\\ud834\\udd1e"', 0, None, True),
292
292
  (u'\U0001d11e', 13))
293
+
294
+ def test_single_char_escape_error_parity(self):
295
+ # Regression: for an invalid single-character escape (``\X``), the C
296
+ # scanstring reported the position of the backslash, while
297
+ # py_scanstring reports the position of the offending escape
298
+ # character itself. Because the "Invalid \\X escape sequence %r"
299
+ # message renders %r as the character at that position, the C path
300
+ # produced the nonsensical "Invalid \\X escape sequence '\\'" (naming
301
+ # the backslash, which is always a valid escape introducer) instead
302
+ # of naming the actual bad character. This asserts exact parity
303
+ # (position and rendered message) between the two backends, matching
304
+ # the \\uXXXX parity already covered by test_escape_error_parity.
305
+ if simplejson.decoder.c_scanstring is None:
306
+ return
307
+
308
+ def get_exc(scanstring, s):
309
+ try:
310
+ scanstring(s, 0, None, True)
311
+ except json.JSONDecodeError as e:
312
+ return (e.pos, str(e).split(': line ')[0])
313
+ return None
314
+
315
+ # Each case: (input, escape_char_index). The rendered message must
316
+ # name the character at escape_char_index, never the backslash.
317
+ cases = [
318
+ (u'\\x41"', 1),
319
+ (u'a\\q"', 2),
320
+ (u'\\ "', 1),
321
+ (u'prefix\\z"', 7),
322
+ (u'\\!"', 1),
323
+ ]
324
+ for s, esc_idx in cases:
325
+ expected = (
326
+ esc_idx,
327
+ "Invalid \\X escape sequence %r" % (s[esc_idx],))
328
+ py = get_exc(simplejson.decoder.py_scanstring, s)
329
+ c = get_exc(simplejson.decoder.c_scanstring, s)
330
+ self.assertEqual(py, expected,
331
+ 'py_scanstring(%r) expected %r, got %r' %
332
+ (s, expected, py))
333
+ self.assertEqual(c, expected,
334
+ 'c_scanstring(%r) expected %r, got %r' %
335
+ (s, expected, c))
336
+ # The reported position must land on the bad char, not the '\\'.
337
+ self.assertNotEqual(s[c[0]], u'\\',
338
+ 'c_scanstring(%r) pointed at the backslash' % (s,))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simplejson
3
- Version: 4.1.1
3
+ Version: 4.1.2
4
4
  Summary: Simple, fast, extensible JSON encoder/decoder for Python
5
5
  Home-page: https://github.com/simplejson/simplejson
6
6
  Author: Bob Ippolito
@@ -1,71 +0,0 @@
1
- import decimal
2
- from decimal import Decimal
3
- from unittest import TestCase
4
- from simplejson.compat import StringIO, reload_module
5
-
6
- import simplejson as json
7
-
8
- class TestDecimal(TestCase):
9
- NUMS = "1.0", "10.00", "1.1", "1234567890.1234567890", "500"
10
- def dumps(self, obj, **kw):
11
- sio = StringIO()
12
- json.dump(obj, sio, **kw)
13
- res = json.dumps(obj, **kw)
14
- self.assertEqual(res, sio.getvalue())
15
- return res
16
-
17
- def loads(self, s, **kw):
18
- sio = StringIO(s)
19
- res = json.loads(s, **kw)
20
- self.assertEqual(res, json.load(sio, **kw))
21
- return res
22
-
23
- def test_decimal_encode(self):
24
- for d in map(Decimal, self.NUMS):
25
- self.assertEqual(self.dumps(d, use_decimal=True), str(d))
26
-
27
- def test_decimal_decode(self):
28
- for s in self.NUMS:
29
- self.assertEqual(self.loads(s, parse_float=Decimal), Decimal(s))
30
-
31
- def test_stringify_key(self):
32
- for d in map(Decimal, self.NUMS):
33
- v = {d: d}
34
- self.assertEqual(
35
- self.loads(
36
- self.dumps(v, use_decimal=True), parse_float=Decimal),
37
- {str(d): d})
38
-
39
- def test_decimal_roundtrip(self):
40
- for d in map(Decimal, self.NUMS):
41
- # The type might not be the same (int and Decimal) but they
42
- # should still compare equal.
43
- for v in [d, [d], {'': d}]:
44
- self.assertEqual(
45
- self.loads(
46
- self.dumps(v, use_decimal=True), parse_float=Decimal),
47
- v)
48
-
49
- def test_decimal_defaults(self):
50
- d = Decimal('1.1')
51
- # use_decimal=True is the default
52
- self.assertRaises(TypeError, json.dumps, d, use_decimal=False)
53
- self.assertEqual('1.1', json.dumps(d))
54
- self.assertEqual('1.1', json.dumps(d, use_decimal=True))
55
- self.assertRaises(TypeError, json.dump, d, StringIO(),
56
- use_decimal=False)
57
- sio = StringIO()
58
- json.dump(d, sio)
59
- self.assertEqual('1.1', sio.getvalue())
60
- sio = StringIO()
61
- json.dump(d, sio, use_decimal=True)
62
- self.assertEqual('1.1', sio.getvalue())
63
-
64
- def test_decimal_reload(self):
65
- # Simulate a subinterpreter that reloads the Python modules but not
66
- # the C code https://github.com/simplejson/simplejson/issues/34
67
- global Decimal
68
- Decimal = reload_module(decimal).Decimal
69
- import simplejson.encoder
70
- simplejson.encoder.Decimal = Decimal
71
- self.test_decimal_roundtrip()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes