Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e0018d6
Fix free-threading builds: convert to heap types on Python 3.13+
claude Apr 9, 2026
92da64d
Add .claude/ to .gitignore
claude Apr 9, 2026
11d3e2c
Fix mangled setup-python version in CI workflow
claude Apr 9, 2026
729f306
Fix CI: restrict Python 2.7 wheel builds to x86_64
claude Apr 9, 2026
a57ca2d
Move all constants to per-module state for full interpreter isolation
claude Apr 9, 2026
dcb6f76
Add aggregate gate jobs matching branch protection rule names
claude Apr 10, 2026
75b84a6
Remove is_gil_enabled() checks so speedups load under free-threading
claude Apr 10, 2026
33a427d
Unify _speedups_state across all Python versions
claude Apr 10, 2026
2383fa2
Fix Python 2 build after state unification
claude Apr 10, 2026
6239409
Store type objects in _speedups_state on all Python versions
claude Apr 10, 2026
4fff31f
Skip Python 3.8 Windows wheels and PyPy builds in CI
claude Apr 10, 2026
c996de5
Drop deprecated CIBW_ENABLE and revert cp38-win* skip
claude Apr 10, 2026
428d933
Add free-threading and subinterpreter test suites
claude Apr 10, 2026
5d14d7a
Replace per-field helper args with _speedups_state *
claude Apr 11, 2026
d172cb2
Cleanup: consistent state naming, rename raise_errmsg, slim JSON_Accu
claude Apr 11, 2026
16a60b2
Move _speedups_state * to first position in helper signatures
claude Apr 11, 2026
84ed47e
Revert setup-python pin to floating v5 tag
claude Apr 11, 2026
14ad877
Fix pre-existing -Wshadow warnings in _speedups.c
claude Apr 11, 2026
18892c5
Add test_debug_build CI job for Python 3.14 --with-pydebug
claude Apr 11, 2026
6754d61
Bump debug build Python to 3.14.4
claude Apr 11, 2026
d1cec90
Update action versions and drop -Werror from debug build
claude Apr 11, 2026
a42f5b2
Use uv / python-build-standalone for debug Python, fix UB in int_as_s…
claude Apr 11, 2026
312e8e9
Pin setup-uv to v8.0.0
claude Apr 11, 2026
68efd9e
Verify C speedups are wired up, not just importable
claude Apr 11, 2026
165ad57
Harden _speedups.c: multi-phase init on Py3, reload safety, assertions
claude Apr 11, 2026
37e27f1
Add boundary regression test for int_as_string_bitcount
claude Apr 11, 2026
e3b9d92
Matrix test_debug_build over standard and free-threaded variants
claude Apr 11, 2026
2f7cd60
Move ''.join cache from file-scope static into state (Py2 only)
claude Apr 11, 2026
1d3a7e1
Template scan_once / _parse_object / _parse_array / _match_number
claude Apr 11, 2026
b1e5ffa
Drop CIBW_SKIP 'pp*' from the main build_wheels step
claude Apr 11, 2026
30a50c5
Inline encoding in scanstring_str template call
claude Apr 11, 2026
d2f71d0
Assorted cleanups: -Wdecl-after-stmt, macro semicolons, state-routed …
claude Apr 11, 2026
8b6e36e
Add AGENTS.md with tribal knowledge from the 3.14t port
claude Apr 11, 2026
d4f50ff
Follow-up cleanups: encoder_steal_encode helper, scanner LOAD_ATTR ma…
claude Apr 11, 2026
11735a3
Low-risk cleanups: dead locals, dead TODO blocks, int-stringify helpe…
claude Apr 11, 2026
c57b8d3
Fix key_memo write-only bug, add error-path refcount tests, AGENTS.md…
claude Apr 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add test_debug_build CI job for Python 3.14 --with-pydebug
Builds CPython 3.14.0 from source with --with-pydebug, caches the
install prefix (~10 min first run, ~10 sec on cache hit), and runs
the test suite against it. This catches:

- Refcount leaks (via TestRefcountLeaks, auto-enabled when
  sys.gettotalrefcount is present)
- Py_DECREF asserts, NULL-pointer dereferences, and internal
  consistency checks that release builds skip
- Shadow/strict warnings (-Wall -Wextra -Wshadow -Werror)

Stock Ubuntu python3-dbg only offers 3.12, and neither
actions/setup-python nor the deadsnakes PPA ships -dbg packages
for 3.13/3.14, so building from source (with caching) is the
only route to a 3.14 debug interpreter.

New simplejson/tests/test_speedups.py::TestRefcountLeaks covers:
- dumps / loads round-trip
- Scanner / Encoder construction
- Error paths in scanner_new / encoder_new (module_ref release)

The class is guarded by @skipUnless(hasattr(sys, 'gettotalrefcount'))
so it's inert on release builds and skips silently.

gate_ubuntu now depends on test_debug_build so the aggregate job
used by branch protection waits for it too.

TSan and a suppression-clean valgrind run would both require
further custom CPython builds and remain future work.

https://claude.ai/code/session_01EoWzUsmRRvrZBF2nwQhF95
  • Loading branch information
claude committed Apr 11, 2026
commit 18892c5ebcf48c9f95b738397fac63a41e7d8e9d
59 changes: 58 additions & 1 deletion .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,63 @@ jobs:
- name: Run tests with GIL disabled
run: PYTHON_GIL=0 python -m simplejson.tests._cibw_runner .

test_debug_build:
name: Tests on Python 3.14 debug build
runs-on: ubuntu-latest
env:
# Pin the Python version so the cache key is stable. Bump when
# we want to pick up a new 3.14.x release.
PYTHON_VERSION: '3.14.0'
PYTHON_PREFIX: /opt/python-debug
steps:
- uses: actions/checkout@v4

- name: Cache Python debug build
id: cache-python
uses: actions/cache@v4
with:
path: ${{ env.PYTHON_PREFIX }}
key: python-${{ env.PYTHON_VERSION }}-debug-${{ runner.os }}-v1

- name: Build Python ${{ env.PYTHON_VERSION }} with --with-pydebug
if: steps.cache-python.outputs.cache-hit != 'true'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential libssl-dev zlib1g-dev libbz2-dev \
libreadline-dev libsqlite3-dev libffi-dev liblzma-dev
curl -fsSL -o Python.tar.xz \
"https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tar.xz"
tar xf Python.tar.xz
cd Python-${PYTHON_VERSION}
./configure --with-pydebug --disable-test-modules \
--prefix="${PYTHON_PREFIX}" --with-ensurepip=install
make -j "$(nproc)"
sudo make install
sudo chown -R "$(id -u):$(id -g)" "${PYTHON_PREFIX}"

- name: Add debug Python to PATH
run: echo "${PYTHON_PREFIX}/bin" >> "$GITHUB_PATH"

- name: Verify debug build
run: |
python3 -c "
import sys
assert hasattr(sys, 'gettotalrefcount'), (
'not a debug build (sys.gettotalrefcount missing)')
print('OK:', sys.version)
"

- name: Build extension with strict warnings (-Werror)
env:
CFLAGS: "-Wall -Wextra -Wshadow -Wstrict-prototypes -Werror -Wno-unused-parameter -Wno-missing-field-initializers -Wno-cast-function-type"
run: |
python3 -m pip install --upgrade setuptools wheel
REQUIRE_SPEEDUPS=1 python3 setup.py build_ext -i

- name: Run tests (TestRefcountLeaks auto-enabled on debug build)
run: python3 -m simplejson.tests._cibw_runner .

build_wheels:
name: Build wheels (${{ matrix.os }}, ${{ matrix.arch }})
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -151,7 +208,7 @@ jobs:
# Aggregate gate jobs to match branch protection rule names
gate_ubuntu:
name: Build wheels on ubuntu-latest
needs: [build_wheels, test_pure_python, test_free_threading]
needs: [build_wheels, test_pure_python, test_free_threading, test_debug_build]
runs-on: ubuntu-latest
steps:
- run: echo "All ubuntu-latest checks passed"
Expand Down
71 changes: 71 additions & 0 deletions simplejson/tests/test_speedups.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,74 @@ def test_encoder_instances_work(self):
"""Verify Encoder heap type instances encode correctly."""
result = simplejson.dumps({"a": 1}, sort_keys=True)
self.assertEqual(result, '{"a": 1}')


@unittest.skipUnless(hasattr(sys, "gettotalrefcount"),
"debug build required (sys.gettotalrefcount)")
class TestRefcountLeaks(TestCase):
"""Catch refcount leaks in the C extension.

These tests only run on debug builds of CPython, which expose
sys.gettotalrefcount(). On release builds they skip silently.
"""

ITER = 2000

def _assert_no_leak(self, func):
import gc
# Warm up to stabilize caches (interned strings, module state, etc.)
for _ in range(50):
func()
gc.collect()
before = sys.gettotalrefcount()
for _ in range(self.ITER):
func()
gc.collect()
after = sys.gettotalrefcount()
delta = after - before
# Allow a small slack for debug build internals, but any real
# leak would grow linearly with ITER.
self.assertLess(abs(delta), 50,
"refcount delta=%d over %d iterations"
% (delta, self.ITER))

@skip_if_speedups_missing
def test_dumps_no_leak(self):
data = {"a": [1, 2, 3], "b": "hello", "c": None, "d": True}
self._assert_no_leak(lambda: simplejson.dumps(data))

@skip_if_speedups_missing
def test_loads_no_leak(self):
raw = '{"a": [1, 2, 3], "b": "hello", "c": null, "d": true}'
self._assert_no_leak(lambda: simplejson.loads(raw))

@skip_if_speedups_missing
def test_scanner_construction_no_leak(self):
self._assert_no_leak(lambda: simplejson.JSONDecoder())

@skip_if_speedups_missing
def test_encoder_construction_no_leak(self):
self._assert_no_leak(lambda: simplejson.JSONEncoder())

@skip_if_speedups_missing
def test_failed_construction_no_leak(self):
"""Error path in scanner_new/encoder_new must release module_ref."""
class BadBool:
def __bool__(self):
raise ZeroDivisionError()
__nonzero__ = __bool__

def try_bad_scanner():
try:
decoder.JSONDecoder(strict=BadBool()).decode('{}')
except ZeroDivisionError:
pass

def try_bad_encoder():
try:
encoder.JSONEncoder(skipkeys=BadBool()).encode({})
except ZeroDivisionError:
pass

self._assert_no_leak(try_bad_scanner)
self._assert_no_leak(try_bad_encoder)
Loading