Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 10 additions & 4 deletions .github/workflows/build-rust-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ jobs:
publish:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: [linux, windows, macos, sdist]
# Never publish artifacts that have not passed the built-wheel matrix.
needs: [linux, windows, macos, sdist, test]
if: startsWith(github.ref, 'refs/tags/rust-v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true')
environment:
name: pypi
Expand Down Expand Up @@ -180,13 +181,17 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.10', '3.11', '3.12', '3.13']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
include:
- os: ubuntu-latest
python-version: '3.15.0-beta.3'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true

- name: Download wheels
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand All @@ -197,15 +202,16 @@ jobs:

- name: Install wheel
run: |
pip install --find-links dist json2xml_rs
pip install --no-index --only-binary=:all: --find-links dist json2xml_rs
pip install pytest

- name: Test import
run: |
python -c "import importlib.metadata as m, os; ref = os.environ['GITHUB_REF_NAME']; expected = ref.removeprefix('rust-v'); actual = m.version('json2xml-rs'); assert not ref.startswith('rust-v') or actual == expected, (actual, expected)"
python -c "from json2xml_rs import dicttoxml; print('Import successful!')"
python -c "from json2xml_rs import dicttoxml; result = dicttoxml({'test': 'value'}); print(result.decode())"

- name: Run tests
run: |
pip install -e .
pytest tests/test_rust_dicttoxml.py -v
pytest -o addopts='' tests/test_rust_dicttoxml.py -v
3 changes: 2 additions & 1 deletion .github/workflows/publish-to-live-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ permissions:
jobs:
build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to pypi
# Rust releases use rust-v* tags and have their own wheel workflow.
if: startsWith(github.event.release.tag_name, 'v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Expand All @@ -35,7 +37,6 @@ jobs:
.

- name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
user: __token__
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:

- name: Run tests
run: |
pytest --cov=json2xml --cov-report=xml:coverage/reports/coverage.xml --cov-report=term -xvs tests -n auto
pytest --cov=json2xml --cov-report=xml:coverage/reports/coverage.xml --cov-report=term --cov-fail-under=100 -xvs tests -n auto
env:
PYTHONPATH: ${{ github.workspace }}

Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,18 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['3.10', '3.11', '3.12', '3.13']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
include:
- os: ubuntu-latest
python-version: '3.15.0-beta.3'
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true

- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
Expand Down Expand Up @@ -99,10 +103,10 @@ jobs:
python -c "from json2xml.dicttoxml_fast import get_backend; print(f'Backend: {get_backend()}')"

- name: Run Rust-specific tests
run: pytest tests/test_rust_dicttoxml.py -v
run: pytest -o addopts='' tests/test_rust_dicttoxml.py -v

- name: Run full test suite
run: pytest tests/ -v --ignore=tests/test_cli.py
run: pytest -o addopts='' tests/ -v --ignore=tests/test_cli.py

benchmark:
name: Performance Benchmark
Expand Down
2 changes: 1 addition & 1 deletion AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Build/Test Commands
- Test: `pytest -vv` (all tests) or `pytest tests/test_<name>.py -vv` (single test file)
- Test with coverage: `pytest --cov=json2xml --cov-report=xml:coverage/reports/coverage.xml --cov-report=term -xvs`
- Test with coverage: `pytest --cov=json2xml --cov-report=xml:coverage/reports/coverage.xml --cov-report=term --cov-fail-under=100 -xvs`
- Lint: `ruff check json2xml tests`
- Type check: `uvx ty check json2xml tests`
- Test all Python versions: `tox`
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ typecheck: ## check types with ty
uvx ty check json2xml tests

test: ## run tests quickly with the default Python
pytest --cov=json2xml --cov-report=xml:coverage/reports/coverage.xml --cov-report=term -xvs tests
pytest --cov=json2xml --cov-report=xml:coverage/reports/coverage.xml --cov-report=term --cov-fail-under=100 -xvs tests

test-simple: ## run tests without coverage
pytest -vv tests
Expand Down
31 changes: 31 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
# json2xml_rs 0.4.2

Released 2026-07-15.

## Highlights

- Replaced scalar XML escape-byte scanning with `memchr` word/SIMD-optimized searches for the five XML escape characters.
- Preserved the bounded 16 KiB streaming writer after a 4–128 KiB capacity sweep found no benefit from increasing it.
- Kept the serialized output byte-for-byte identical at 4,093,244 bytes for the release benchmark payload.

## Performance

The paired CPython 3.15.0b3 release benchmark serialized the deterministic 5,000-record payload in 21 rounds of 50 conversions.

| Metric | Before | After | Change |
| --- | ---: | ---: | ---: |
| Median conversion | 6.007 ms | 5.632 ms | 6.23% lower |
| Mean conversion | 6.013 ms | 5.643 ms | 6.14% lower |
| Escape scanner exclusive samples | 14.31% | 7.97% | 44.3% lower share |

The committed [before](docs/flamegraphs/rust-before.svg) and [after](docs/flamegraphs/rust-after.svg) flamegraphs show the reduced escape-scanner cost. Rejected tag-building and dispatch experiments regressed the same workload by 6–38%, so this release keeps the optimization deliberately narrow.

## Package Version

- Rust accelerator: `json2xml-rs==0.4.2`

## Verification

The release is gated on Rust formatting and Clippy checks, Rust unit tests, the full Python suite, and built-wheel tests for Linux, macOS, and Windows before PyPI publication.


# json2xml 6.4.0 and json2xml_rs 0.4.1

Released 2026-07-13.
Expand Down
44 changes: 44 additions & 0 deletions docs/flamegraphs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Serializer flamegraphs

## Rust accelerator

These profiles compare the Rust serializer before and after vectorized XML escape-byte scanning on CPython 3.15.0b3.

The workload serializes a deterministic 5,000-record nested payload with type attributes enabled. Samply captured symbolized native stacks for eight seconds, and Inferno rendered the SVG flamegraphs. A separate paired release benchmark used 21 rounds of 50 conversions.

| Metric | Before | After | Change |
| --- | ---: | ---: | ---: |
| Median conversion | 6.007 ms | 5.632 ms | 6.23% lower |
| Mean conversion | 6.013 ms | 5.643 ms | 6.14% lower |
| Escape scanner exclusive samples | 14.31% | 7.97% | 44.3% lower share |

### Rust before

[![Rust serializer flamegraph before optimization](rust-before.svg)](rust-before.svg)

### Rust after

[![Rust serializer flamegraph after optimization](rust-after.svg)](rust-after.svg)

### Output buffer capacity

A post-optimization sweep tested 4, 8, 16, 32, 64, and 128 KiB capacities. The useful range plateaued at 16–64 KiB; an ABBA-interleaved 16-vs-32 KiB confirmation measured 5.974 ms versus 6.024 ms median, so the existing 16 KiB capacity remains the best measured choice without increasing per-call memory.

## Pure Python serializer

These profiles compare the pure-Python serializer before and after native JSON type fast paths on CPython 3.15.0b3.

The workload serializes a deterministic 5,000-record nested payload 20 times with type attributes enabled. Python 3.15's tracing profiler captured the call tree, and FlameProf rendered the SVGs.

| Profile | Traced time | Function calls | `isinstance` calls |
| --- | ---: | ---: | ---: |
| Before | 8.311 s | 48.17 million | 11.70 million |
| After | 5.782 s | 30.13 million | 2.80 million |

### Python before

[![Serializer flamegraph before optimization](python315-before.svg)](python315-before.svg)

### Python after

[![Serializer flamegraph after optimization](python315-after.svg)](python315-after.svg)
Loading
Loading