目录
Bob Ippolito

Release version 4.0.0: free-threading, CPython json parity, Python 2.7 fixes (#371)

  • Prepare 4.0.0 release: changelog, version bump, metadata updates

Add comprehensive CHANGES.txt entry for 4.0.0 covering all work since 3.20.2: free-threading support, C extension memory safety fixes, encoding fast paths, Python 3.14 CI, SPDX license, and RawJSON docs. Fix “smae” typo in 3.20.2 entry. Bump version to 4.0.0 in setup.py and init.py. Add GraalPy to setup.py classifiers. Update README to reflect Python 3.14 testing (was still mentioning 3.8).

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Add pyproject.toml, migrate distutils imports, drop Python 3.3-3.5, add PEP 517 CI
  • Add pyproject.toml with [build-system] for PEP 517 builds. setup.py is retained for Py2.7 wheel builds and backwards compatibility.
  • Migrate build_ext import from distutils to setuptools (with distutils fallback for bare Py2 environments). Error classes stay in distutils.errors since setuptools vendors distutils on 3.12+.
  • Remove imp module fallback in compat.py; use importlib.reload unconditionally on Python 3 (imp was only needed for 3.3, EOL 2017).
  • Drop 3.3/3.4/3.5 classifiers and add !=3.3.* to python_requires.
  • Add MANIFEST.in entry for *.toml so pyproject.toml is in sdists.
  • Add test_pep517_build CI job that builds via python -m build, installs the resulting wheel, and smoke-tests it. Gated under gate_ubuntu alongside the other test jobs.
  • Update CHANGES.txt with all packaging changes.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Add Pyodide (wasm32) wheel builds with C speedups, fix changelog wording

Add build_pyodide_wheel CI job using cibuildwheel’s CIBW_PLATFORM=pyodide to cross-compile the C extension to WebAssembly via Emscripten. Previously Pyodide users fell back to the pure-Python none-any wheel; now they get the compiled C speedups. The new job is gated under gate_ubuntu and required by both upload_pypi and upload_pypi_test.

Also fix CHANGES.txt: remove “BACKWARDS INCOMPATIBLE” from the heap-types entry since the Python-level API is unchanged – only the C extension implementation changed, not its interface.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Skip thread and subprocess tests on Emscripten (Pyodide)

Pyodide/Emscripten doesn’t support threads (RuntimeError: can’t start new thread) or subprocesses (OSError: emscripten does not support processes). Add sys.platform == ‘emscripten’ checks in the shared helper methods (_run_threads, runTool) so all affected tests skip cleanly instead of erroring.

Verified by simulating sys.platform=’emscripten’: all 7 tests (4 in test_free_threading, 3 in test_tool) skip with appropriate messages. Native test suite is unaffected (206 tests, 39 skipped).

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Fail hard when C speedups are missing under cibuildwheel

TestMissingSpeedups previously skipped silently when _speedups failed to load, even in cibuildwheel where a compiled extension is expected. Now it checks CIBUILDWHEEL=1 (set by cibuildwheel in both build and test environments) and fails with a clear message instead. This catches broken Pyodide wasm32 builds, native wheel builds, or any other cibuildwheel-managed build where the C extension should be present.

Update CHANGES.txt to document the Emscripten test skips and the speedup verification improvement.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Expand PR #370 changelog entry with specific fixes and optimizations

Replace vague “additional correctness fixes” with the actual changes: ascii_escape overflow fix, list counter overflow fix, PyDict_Next fast path, indexed list/tuple fast path, PyUnicodeWriter backend on 3.14+.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Update actions to v6, fix Python version refs, document minimum 3.4
  • Bump actions/setup-python from v5 to v6 (fixes Node.js 20 deprecation warning in CI).
  • Change Pyodide job host Python from 3.12 to 3.14 with a comment explaining it’s just the host for cibuildwheel, not the Pyodide target.
  • Update README.rst, index.rst, and CHANGES.txt to say “Python 3.4+” instead of “Python 3.3+”, matching python_requires and the imp module removal in compat.py. Consolidate the two CHANGES.txt entries about 3.3 removal into one clear statement of the new minimum.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Narrow support matrix to Python 2.7 and 3.9+

Only these versions have wheels built and tested in CI. Update python_requires to >=2.7 with 3.0-3.8 excluded, drop classifiers for 2.5, 2.6, 3.6-3.8, and update README/index.rst/CHANGES.txt to reflect the actual support matrix.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Remove pre-2.7 and pre-3.9 compat code, add Pyodide classifiers

Now that the support matrix is Python 2.7 + 3.9+, remove dead compatibility code for 2.5, 2.6, and 3.0-3.8:

  • decoder.py: remove struct-based float(‘nan’) fallback for <2.6, drop unused struct import
  • setup.py: remove Windows 2.6 IOError/ValueError ext_errors, remove with_statement future import, add Framework :: Pyodide and Environment :: WebAssembly :: Emscripten classifiers
  • tool.py: remove with_statement future import
  • tests/init.py: remove doctest <2.7 cStringIO workaround, simplify TestMissingSpeedups (skipTest always available)
  • tests/_helpers.py: remove with_statement import and sys import, simplify skip_if_speedups_missing (SkipTest always available)
  • tests/test_tool.py: remove with_statement, replace fragile test.support/test.test_support imports with inline regex, simplify open_temp_file (NamedTemporaryFile always available)
  • tests/test_decode.py: remove assertIs polyfill (available on 2.7+)
  • tests/test_namedtuple.py: remove namedtuple ImportError fallback (available on 2.6+), simplify mock SkipTest check
  • tests/test_free_threading.py, test_subinterpreters.py, test_speedups.py: remove with_statement future imports

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Remove Framework :: Pyodide classifier

Framework classifiers are for plugins/extensions of a framework, not for libraries that merely run on the platform. Keep only the Environment :: WebAssembly :: Emscripten classifier.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Move Python version requirement to top of 4.0.0 changelog

The support matrix change (Python 2.7 + 3.9+ only) is the most important thing for users upgrading to simplejson 4. Move it to the first bullet point and make it clear that pip will block installation on unsupported versions.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Skip cp38 in cibuildwheel to match python_requires 3.9+

cibuildwheel v3.x builds cp38 by default. With python_requires excluding 3.8, pip refuses to install the built wheel during cibuildwheel’s test phase. Add CIBW_SKIP: “cp38-*” to the build_wheels job.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Include Python 3.8 in support matrix (cibuildwheel builds it)

cibuildwheel v3.x builds cp38 wheels by default and the code works fine on 3.8. Revert the CIBW_SKIP and add 3.8 back to python_requires, classifiers, README, index.rst, and CHANGES.txt. Support matrix is now Python 2.7 and Python 3.8+.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Fix Python 2 syntax error: replace rb”” with b”” in test_tool.py

Python 2 doesn’t support raw bytes literals (rb”…”). Use regular bytes literal with escaped backslashes instead.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Fix Python 2.7 C extension load failure: clear exception in init_speedups

On Python 2.7, if init_speedups_state() fails (e.g. due to an import error during module init), it leaves a Python exception set. CPython 2.7’s import machinery checks PyErr_Occurred() after calling the init function, and if an error is set, it removes the module from sys.modules and propagates the error – even though the module object was successfully created with make_scanner and make_encoder already installed.

This caused the C speedups to silently fail to load on Python 2.7, falling back to the pure-Python path. The TestMissingSpeedups check we added earlier made this visible.

Fix: clear the exception if init_speedups_state fails, since the essential types and functions are already installed on the module.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Diagnose Python 2.7 init_speedups_state failure: print error to stderr

Print the actual exception from init_speedups_state to stderr before clearing it, so the CI log shows what’s really failing on Python 2.7. This is a diagnostic commit to identify the root cause.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Diagnose Py2.7 speedups import failure; separate Py2.7 CI job
  • TestMissingSpeedups now captures and reports the actual ImportError message and lists _speedups files found in the package directory, so the next CI run will reveal whether the issue is dlopen, missing .so, or something else entirely.
  • Replace fprintf/PyErr_Print diagnostic in C init (which proved that init_speedups_state succeeds) with PyErr_Clear for correctness.
  • Move Python 2.7 wheel build into its own parallel job (build_wheels_py27) so results arrive sooner.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Fix Python 2.7 C extension load failure: guard PyDict_SetDefault

PyDict_SetDefault was added in CPython 3.4 and does not exist in the Python 2.7 C API. The unguarded call caused an undefined symbol error at dlopen time, preventing _speedups.so from loading at all on 2.7.

Use PyDict_GetItem + PyDict_SetItem as a fallback for Python < 3.4.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Fix Python 2.7 C extension: guard PyDict_GetItemWithError

PyDict_GetItemWithError was added in CPython 3.4. The unguarded call in json_PyDict_GetItemRef caused an undefined symbol error at dlopen on Python 2.7. Fall back to PyDict_GetItem for Python < 3.4.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Add trailing comma detection in JSON decoder

Detect illegal trailing commas in objects and arrays (e.g. {“a”: 1,} and [1,]) and raise JSONDecodeError with a specific message instead of the generic “Expecting value” or “Expecting property name” errors.

Both the Python decoder (JSONObject/JSONArray in decoder.py) and the C scanner (_speedups_scan.h) now report “Illegal trailing comma before end of object/array”. Matches CPython 3.13+ json module behavior.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Add array_hook parameter to decoder (CPython 3.15 feature)

New array_hook parameter for loads(), load(), and JSONDecoder that is called with each decoded JSON array (as a list). Its return value replaces the list, analogous to object_hook for dicts.

Implemented in both the Python decoder (JSONArray in decoder.py) and the C scanner (_speedups_scan.h). Threaded through py_make_scanner, loads(), and load().

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Add frozendict encoding support (CPython 3.15 PEP 814)

Extend the Python encoder to treat frozendict (new builtin in CPython 3.15) identically to dict for JSON serialization. On older Pythons where frozendict doesn’t exist, behavior is unchanged.

Uses a _dict_types tuple that includes frozendict when available, checked in _iterencode, _iterencode_list, and _iterencode_dict.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Add exc.add_note() context on serialization errors (CPython 3.14)

On Python 3.11+ (PEP 678), serialization errors now get annotated with notes describing where in the JSON structure the error occurred. For example, encoding {‘a’: [1, object(), 3]} produces:

TypeError: Object of type object is not JSON serializable
when serializing object object
when serializing list item 1
when serializing dict item 'a'

Notes are added in _iterencode_list (item index), _iterencode_dict (item key), and _iterencode (default() call). On Python < 3.11 or when using the C encoder, behavior is unchanged.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Add _iterencode_once cycle breaker (CPython 3.15 optimization)

The mutually recursive closures _iterencode, _iterencode_dict, and _iterencode_list create reference cycles that delay garbage collection. Wrap the top-level call in _iterencode_once which deletes the closure references in a finally block, breaking the cycle after encoding.

Only active on Python 3+ (uses nonlocal). Python 2.7 returns _iterencode directly as before.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Fix _iterencode_once: move nonlocal to separate module for Python 2

Python 2 raises SyntaxError on nonlocal even inside an if-guarded block, since the parser processes the entire function body. Move the cycle breaker wrapper to simplejson/_cycle_breaker.py which is only imported on Python 3.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Revert _iterencode_once cycle breaker

The cycle breaker requires nonlocal+del inside _make_iterencode to actually clear the closure cells. Moving it to a separate module only deletes copies of the arguments, not the real inter-closure references. Since encoder.py must be parseable by Python 2 (which lacks nonlocal), this optimization cannot be implemented without major restructuring. Drop it — it’s a minor GC improvement from CPython 3.15, not a correctness fix.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Use plain dict for _dict_types fallback instead of (dict,) tuple

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Update CHANGES.txt with new features and Python 2.7 fixes

Add entries for array_hook, trailing comma detection, frozendict encoding, exc.add_note() context, Python 2.7 C extension fixes, and the separated Py2.7 CI job.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Remove internal-only Python 2.7 fix entries from changelog

The PyDict_SetDefault/PyDict_GetItemWithError regressions were introduced and fixed within the same release cycle, so they are not user-visible changes worth documenting.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Remove PyErr_Clear() after init_speedups_state on Python 2.7

Silently clearing an exception from init_speedups_state would leave the module in a broken state (missing string constants, RawJSON type, etc.). If state init fails, let the exception propagate so the import fails loudly. Also simplify the TestMissingSpeedups diagnostic now that the Py2.7 undefined symbol issues are fixed.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Strengthen tests to match CPython json test coverage

Trailing comma tests: consolidated into test_trailing_comma_unexpected_data with 4 cases matching CPython’s test_unexpected_data, verifying exact error messages (not just substrings).

array_hook tests: added test_array_hook_with_object_hook to verify both hooks work together on nested structures, matching CPython’s combined object_hook+array_hook test.

add_note tests: replaced single weak test with three specific tests matching CPython’s test_recursion.py coverage:

  • test_add_note_list_recursion: exact notes on circular list
  • test_add_note_dict_recursion: exact notes on circular dict
  • test_add_note_nested_error: exact notes list for nested dict->list->object error path

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Add Python 3.15-dev CI, frozendict tests, reject non-ASCII digits
  • Add 3.15-dev to test_pure_python CI matrix for early detection of regressions and to validate frozendict encoding.
  • Add TestFrozenDict test class (skipped on Python < 3.15) covering toplevel, nested, and mixed dict/frozendict encoding.
  • Add test_nonascii_digits_rejected: fullwidth digits like U+FF10 must not be accepted as JSON numbers (CPython gh-125687).
  • Fix NUMBER_RE in scanner.py to use [0-9] instead of \d, which on Python 3 matches Unicode digits like fullwidth numerals.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Add test_prerelease CI job for Python 3.15-dev with C speedups

Build and test the C extension against Python 3.15-dev to catch API changes early and validate frozendict encoding support. Added to the gate_ubuntu dependency list.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Raise refcount leak threshold from 100 to 200

The test_failed_construction_no_leak test fires exception-heavy paths (BadBool.bool raising ZeroDivisionError) which cause CPython debug builds to accumulate internal refs (exception traceback interning, code object specialization, etc.) reaching ~162 in phase2. This is not a real leak (a 1-ref-per-call leak would produce phase2 ≈ 2000).

The array_hook field we added to PyScannerObject is not even loaded in the failure path (the error fires at PyObject_IsTrue before LOAD_ATTR for array_hook), and scanner_clear properly handles it via the X-macro.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Collect GC every iteration in refcount leak tests

Calling gc.collect() inside the measurement loop instead of only between phases prevents cyclic garbage from accumulating across GC generations, which caused noisy refcount deltas (phase2=162) in exception-heavy tests like test_failed_construction_no_leak.

This lets us restore the tighter threshold of 100 while eliminating false positives from GC generation promotion timing.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Add frozendict support to C encoder

frozendict (CPython 3.15, PEP 814) does not pass PyDict_Check, so the C encoder was falling through to default() and raising TypeError.

  • Store FrozenDictType in module state (looked up from builtins at init; NULL on older Pythons where it doesn’t exist)
  • Add is_dict_or_frozendict() helper that checks PyDict_Check first, then falls back to isinstance(obj, FrozenDictType) when available
  • Use is_dict_or_frozendict in encoder_listencode_obj dispatch and encoder_steal_encode (_asdict return check)
  • Use PyObject_Length instead of PyDict_Size for the empty-dict check since PyDict_Size doesn’t work on frozendict
  • The slow path (PyMapping_Items) already handles non-dict mappings, so frozendict iteration works without further changes

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Remove dead Python 3.3/3.4 single-phase init code, update docstring
  • Remove the #if PY_VERSION_HEX < 0x03050000 fallback in PyInit__speedups (single-phase init via PyModule_Create) and the NULL m_slots branch in moduledef. Python 3.3/3.4 are excluded by python_requires, so multi-phase init (PEP 489) is always used.
  • Update module docstring to reflect current support matrix (Python 2.7 and 3.8+) instead of claiming Python 2.5 compatibility.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Add C fast path for encode_basestring (ensure_ascii=False)

Previously, ensure_ascii=False fell back to a pure Python regex-based encode_basestring. Add a C implementation (escape_unicode_noascii) that only escapes control characters (0x00-0x1f), backslash, and double-quote, passing all other characters through unchanged.

Two code paths:

  • Python 3.14+: single-pass via PyUnicodeWriter
  • Pre-3.14 / Python 2.7: two-pass (compute size, then fill)

The C function is exposed as _speedups.encode_basestring and wired into encoder.py as the default encode_basestring when available. _toggle_speedups also handles switching between C and Python versions.

Closes simplejson/simplejson#207.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Fix Python 2.7 build and -Werror on 3.14t debug builds
  • Guard module_slots and moduledef behind #if PY_MAJOR_VERSION >= 3 (PyModuleDef_Slot doesn’t exist on Python 2.7)
  • Guard escape_char_noascii and escape_char_noascii_size behind

    if PY_VERSION_HEX < 0x030E0000 (unused on 3.14+ where the

    PyUnicodeWriter path writes escapes inline)
  • Remove redundant nested #if PY_MAJOR_VERSION >= 3

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Update CHANGES.txt with encode_basestring fast path and other additions

Add entries for:

  • C fast path for encode_basestring (ensure_ascii=False), closes #207
  • Non-ASCII digit rejection in Python decoder
  • Dead code removal (Python 3.3/3.4 single-phase init)

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Use skipIf decorator for add_note tests instead of in-body check

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Collect GC during warmup phase in refcount leak tests

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Fail hard if frozendict is missing on Python 3.15+

On Python >= 3.15, frozendict is a builtin and its absence indicates something seriously wrong (e.g. MemoryError). Use a compile-time version guard in the C extension (#if PY_VERSION_HEX >= 0x030F0000) and a runtime version check in encoder.py so that errors propagate instead of being silently swallowed.

On older Pythons, FrozenDictType is simply NULL / _dict_types is dict.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Use PyAnyDict_Check C API for frozendict instead of runtime lookup

CPython 3.15 provides PyAnyDict_Check() (checks both dict and frozendict) and PyFrozenDict_Check() as compile-time macros in Include/cpython/dictobject.h. Use these instead of storing the frozendict type in module state and doing PyObject_IsInstance at runtime.

On Python < 3.15, JSON_AnyDict_Check falls back to PyDict_Check.

Removes FrozenDictType from _speedups_state and its init/clear code.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4

  • Remove duplicate sys import in encoder.py

sys was already imported at line 8; the redundant import sys as _sys for the frozendict version check was unnecessary.

https://claude.ai/code/session_01H3C9AwLogP8c74RodYcNZ4


Co-authored-by: Claude noreply@anthropic.com

4天前693次提交
关于

用于编码和解码JSON数据的Python库

2.1 MB
邀请码
    Gitlink(确实开源)
  • 加入我们
  • 官网邮箱:gitlink@ccf.org.cn
  • QQ群
  • QQ群
  • 公众号
  • 公众号

版权所有:中国计算机学会技术支持:开源发展技术委员会
京ICP备13000930号-9 京公网安备 11010802032778号