fastjsonpatch 0.1.2__tar.gz → 0.1.4__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.
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/.github/workflows/CI.yml +19 -1
- fastjsonpatch-0.1.4/BENCHMARKS.md +57 -0
- fastjsonpatch-0.1.4/CHANGELOG.md +33 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/Cargo.lock +1 -1
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/Cargo.toml +1 -1
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/MAINTAINING.md +23 -7
- fastjsonpatch-0.1.4/PKG-INFO +112 -0
- fastjsonpatch-0.1.4/README.md +84 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/pyproject.toml +1 -1
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/python/fastjsonpatch/__init__.py +1 -2
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/src/lib.rs +5 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/test_parity.py +54 -0
- fastjsonpatch-0.1.2/CHANGELOG.md +0 -17
- fastjsonpatch-0.1.2/PKG-INFO +0 -206
- fastjsonpatch-0.1.2/README.md +0 -178
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/.gitignore +0 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/LICENSE +0 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/THIRD_PARTY_LICENSES.md +0 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/bench/compare.py +0 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/scripts/test_linux_matrix.sh +0 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/scripts/third_party_licenses.py +0 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/rfc6902/LICENSE-APACHE-2.0 +0 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/rfc6902/README.md +0 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/rfc6902/spec_tests.json +0 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/rfc6902/test_rfc6902.py +0 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/rfc6902/tests.json +0 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/upstream/test_upstream.py +0 -0
- {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/upstream/tests.js +0 -0
|
@@ -44,7 +44,10 @@ jobs:
|
|
|
44
44
|
run: |
|
|
45
45
|
python -m pip install --upgrade pip maturin
|
|
46
46
|
maturin build --release --out dist
|
|
47
|
-
|
|
47
|
+
# Install the wheel just built by its path: with --find-links, pip prefers a
|
|
48
|
+
# PyPI wheel of the same version and platform tag over the local one.
|
|
49
|
+
wheel=$(ls dist/*.whl)
|
|
50
|
+
python -m pip install "${wheel}[test]"
|
|
48
51
|
- name: Tests (parity with jsonpatch, upstream suite, RFC 6902 suite)
|
|
49
52
|
shell: bash
|
|
50
53
|
run: |
|
|
@@ -127,6 +130,21 @@ jobs:
|
|
|
127
130
|
name: sdist
|
|
128
131
|
path: dist
|
|
129
132
|
|
|
133
|
+
# One check for branch protection to require: fails unless every test, wheel and
|
|
134
|
+
# sdist job succeeded.
|
|
135
|
+
ci-ok:
|
|
136
|
+
name: ci-ok
|
|
137
|
+
needs: [test, wheels, sdist]
|
|
138
|
+
if: always()
|
|
139
|
+
runs-on: ubuntu-latest
|
|
140
|
+
steps:
|
|
141
|
+
- name: All required jobs succeeded
|
|
142
|
+
run: |
|
|
143
|
+
echo 'test=${{ needs.test.result }} wheels=${{ needs.wheels.result }} sdist=${{ needs.sdist.result }}'
|
|
144
|
+
test '${{ needs.test.result }}' = success
|
|
145
|
+
test '${{ needs.wheels.result }}' = success
|
|
146
|
+
test '${{ needs.sdist.result }}' = success
|
|
147
|
+
|
|
130
148
|
publish:
|
|
131
149
|
name: publish to ${{ startsWith(github.ref, 'refs/tags/v') && 'PyPI' || 'TestPyPI' }}
|
|
132
150
|
needs: [test, wheels, sdist]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# fastjsonpatch benchmarks
|
|
2
|
+
|
|
3
|
+
## Results
|
|
4
|
+
|
|
5
|
+
Measured on Linux x86_64, Python 3.13 (times are the best of several runs).
|
|
6
|
+
|
|
7
|
+
Real-world data:
|
|
8
|
+
|
|
9
|
+
| scenario | size | jsonpatch | fastjsonpatch | speedup |
|
|
10
|
+
|---|---|---|---|---|
|
|
11
|
+
| Kubernetes OpenAPI v1.33 (3.7 MB) → v1.34 (3.8 MB): make_patch | 283 ops | 136.4 ms | 9.6 ms | **14x** |
|
|
12
|
+
| Kubernetes OpenAPI v1.33 → v1.34: apply | 283 ops | 78.0 ms | 11.4 ms | **7x** |
|
|
13
|
+
| 3-op patch on the v1.34 spec | 3.8 MB | 71.1 ms | 10.1 ms | **7x** |
|
|
14
|
+
| 3-op patch on the v1.34 spec, `in_place=True` | 3.8 MB | 21.2 µs | 4.7 µs | **4x** |
|
|
15
|
+
| PyPI metadata snapshots (8k records): make_patch | 2,487 ops | 255.2 ms | 12.1 ms | **21x** |
|
|
16
|
+
| PyPI metadata snapshots: apply | 2,487 ops | 91.7 ms | 14.0 ms | **7x** |
|
|
17
|
+
| REST resource, apply_patch per request | ×1000 | 47.4 ms | 7.5 ms | **6x** |
|
|
18
|
+
| REST resource, make_patch per request | ×1000 | 74.4 ms | 5.6 ms | **13x** |
|
|
19
|
+
|
|
20
|
+
Synthetic records shaped like `{"id", "name", "score", "tags": [..], "meta": {..}}`:
|
|
21
|
+
|
|
22
|
+
| workload | ops | make_patch: jsonpatch | fastjsonpatch | speedup | apply: jsonpatch | fastjsonpatch | speedup |
|
|
23
|
+
|---|---|---|---|---|---|---|---|
|
|
24
|
+
| 10k records, 100 field edits | 100 | 292.61 ms | 5.26 ms | **56x** | 89.38 ms | 7.90 ms | **11x** |
|
|
25
|
+
| 10k records, insert 1 at front | 49911 | 669.77 ms | 42.78 ms | **16x** | 535.21 ms | 72.35 ms | **7x** |
|
|
26
|
+
| 10k records, delete 50 | 44900 | 636.53 ms | 40.46 ms | **16x** | 563.20 ms | 63.45 ms | **9x** |
|
|
27
|
+
| 10k records, move 20 | 26305 | 434.93 ms | 24.38 ms | **18x** | 325.64 ms | 41.03 ms | **8x** |
|
|
28
|
+
| 2k ints, insert 1 at front | 1 | 17.04 ms | 1.91 ms | **9x** | 581 µs | 21 µs | **27x** |
|
|
29
|
+
| small config (200 keys), 1 edit | 1 | 1.98 ms | 35 µs | **57x** | 464 µs | 32 µs | **15x** |
|
|
30
|
+
|
|
31
|
+
Python 3.14 made `copy.deepcopy` about 1.5x faster, so on 3.14 jsonpatch's `apply` on
|
|
32
|
+
a large document is faster too; on the "10k records, 100 field edits" workload the
|
|
33
|
+
`apply` speedup drops from about 11x to about 6x. The `make_patch` speedup stays about
|
|
34
|
+
the same. Run `python bench/compare.py` (from the source distribution) to measure on
|
|
35
|
+
your own machine.
|
|
36
|
+
|
|
37
|
+
## Where the time goes
|
|
38
|
+
|
|
39
|
+
**make_patch.** jsonpatch walks both documents in Python and compares every scalar
|
|
40
|
+
with `json.dumps(a) == json.dumps(b)`. Profiling the "10k records" workload with
|
|
41
|
+
cProfile puts about 40% of the time in `json.dumps` and about a third in `isinstance`
|
|
42
|
+
checks against abstract base classes (`MutableMapping`, `MutableSequence`).
|
|
43
|
+
fastjsonpatch walks the documents in Rust over the CPython objects directly (no
|
|
44
|
+
conversion to a Rust data structure), compares values natively with the same rules as the `json.dumps` comparison (`1`, `1.0` and `True`
|
|
45
|
+
are all different, `-0.0 != 0.0`, NaN equals NaN), keeps the current path on a stack,
|
|
46
|
+
and takes a single lockstep pass when two dicts have the same keys in the same order.
|
|
47
|
+
It is a port of jsonpatch's `DiffBuilder`, so it produces the same operations in the
|
|
48
|
+
same order, including detected moves.
|
|
49
|
+
|
|
50
|
+
**apply.** jsonpatch first runs `copy.deepcopy` on the document (so the original is
|
|
51
|
+
not modified), then builds a `JsonPointer` and an operation object for each operation.
|
|
52
|
+
For a small patch on a large document the copy is nearly all of the time.
|
|
53
|
+
fastjsonpatch copies each dict and list with one C call and only recurses into values
|
|
54
|
+
that are themselves containers; it keeps `deepcopy`'s semantics (shared and cyclic
|
|
55
|
+
references are copied once) but skips the bookkeeping for containers referenced only
|
|
56
|
+
once. It then resolves the paths and runs the operations without creating
|
|
57
|
+
`JsonPointer` or operation objects.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.4 (2026-10-12)
|
|
4
|
+
|
|
5
|
+
- `import fastjsonpatch` takes about 11 ms instead of about 53 ms: `__version__` now
|
|
6
|
+
comes from the Rust module instead of `importlib.metadata`, which is no longer
|
|
7
|
+
imported. (jsonpatch 1.34 takes about 11 ms.)
|
|
8
|
+
- CI: the macOS and Windows test jobs installed the wheel from PyPI instead of the
|
|
9
|
+
one built in the job whenever both had the same version; they now install the
|
|
10
|
+
built wheel by path. No change to the package.
|
|
11
|
+
|
|
12
|
+
## 0.1.3 (2026-10-12)
|
|
13
|
+
|
|
14
|
+
- Fix: `replace` with a path ending in `-` that names a dict key (`{"-": 1}`,
|
|
15
|
+
path `/-`) was applied; jsonpatch 1.34 raises `InvalidJsonPatch` for it, and
|
|
16
|
+
fastjsonpatch now does too.
|
|
17
|
+
- Shorter README; full benchmark tables moved to BENCHMARKS.md.
|
|
18
|
+
|
|
19
|
+
## 0.1.2 (2026-10-11)
|
|
20
|
+
|
|
21
|
+
Documentation only: the README now states only measured facts (including exactly
|
|
22
|
+
which platforms CI tests), and its link to MAINTAINING.md works on PyPI. No code
|
|
23
|
+
changes. (0.1.1 was tagged but not published.)
|
|
24
|
+
|
|
25
|
+
## 0.1.0 (2026-10-11)
|
|
26
|
+
|
|
27
|
+
First release. Behaves like jsonpatch 1.34 with `make_patch`, `apply`/`apply_patch`
|
|
28
|
+
and patch validation in Rust:
|
|
29
|
+
|
|
30
|
+
- `make_patch` 9-57x faster, `apply` 4-27x faster (see BENCHMARKS.md).
|
|
31
|
+
- Standalone: depends only on `jsonpointer`.
|
|
32
|
+
- Wheels for Linux (glibc and musl, x86_64 and aarch64), macOS (arm64, x86_64) and
|
|
33
|
+
Windows (x64); one abi3 wheel per platform covers Python 3.12+.
|
|
@@ -19,7 +19,7 @@ jsonpatch 1.34. A behaviour difference is a bug, unless it is listed in the READ
|
|
|
19
19
|
```sh
|
|
20
20
|
maturin develop --release
|
|
21
21
|
pip install "jsonpatch==1.34" pytest
|
|
22
|
-
pytest tests # ~
|
|
22
|
+
pytest tests # ~1580 tests, a few seconds
|
|
23
23
|
python bench/compare.py # speed vs jsonpatch
|
|
24
24
|
```
|
|
25
25
|
|
|
@@ -33,6 +33,19 @@ python bench/compare.py # speed vs jsonpatch
|
|
|
33
33
|
CI runs the tests and the speed comparison on Linux x86_64/arm64, macOS arm64/x86_64
|
|
34
34
|
and Windows for Python 3.12, 3.13 and 3.14; the run's summary page has the table.
|
|
35
35
|
|
|
36
|
+
## Making changes
|
|
37
|
+
|
|
38
|
+
Every change goes through a pull request; nothing is pushed to `main` directly.
|
|
39
|
+
|
|
40
|
+
1. Branch from `main` (`fix/...`, `feat/...`, `chore/...`), commit, push the branch.
|
|
41
|
+
2. Open a pull request. CI runs on it; the `ci-ok` check passes only when every
|
|
42
|
+
test, wheel and sdist job passed.
|
|
43
|
+
3. Review the diff on the pull request page, then merge (squash or merge commit).
|
|
44
|
+
|
|
45
|
+
`main` has a branch rule that requires a pull request and the `ci-ok` check. It does
|
|
46
|
+
not require an approving review: GitHub does not let the author approve their own
|
|
47
|
+
pull request, and the maintainer pushes with their own account.
|
|
48
|
+
|
|
36
49
|
## Versions
|
|
37
50
|
|
|
38
51
|
[Semantic versioning](https://semver.org): `MAJOR.MINOR.PATCH`.
|
|
@@ -46,19 +59,22 @@ and Windows for Python 3.12, 3.13 and 3.14; the run's summary page has the table
|
|
|
46
59
|
While on 0.x the API may still change between minor versions. Go to 1.0 once the
|
|
47
60
|
package has had real-world use without behaviour bugs.
|
|
48
61
|
|
|
49
|
-
The version
|
|
50
|
-
`fastjsonpatch.__version__` reads it from the installed package. `Cargo.toml`
|
|
51
|
-
|
|
62
|
+
The published version comes from `pyproject.toml` (`[project] version`);
|
|
63
|
+
`fastjsonpatch.__version__` reads it from the installed package. Keep `Cargo.toml`
|
|
64
|
+
and `Cargo.lock` at the same version (they ship in the sdist).
|
|
52
65
|
|
|
53
66
|
## Releasing
|
|
54
67
|
|
|
55
|
-
1.
|
|
68
|
+
1. In a pull request, update the version in `pyproject.toml`, `Cargo.toml` (and
|
|
69
|
+
`Cargo.lock`) and `CHANGELOG.md`; merge it with a green `ci-ok`.
|
|
56
70
|
2. Optional dry run: Actions → CI → "Run workflow" with target `testpypi`, then
|
|
57
71
|
`pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ fastjsonpatch`.
|
|
58
72
|
A version number can be uploaded to TestPyPI only once as well, so use a dev
|
|
59
73
|
version (e.g. `0.1.1.dev1`) for dry runs.
|
|
60
|
-
3. `git tag v0.1.1 && git push origin v0.1.1`. CI checks
|
|
61
|
-
version
|
|
74
|
+
3. On the merged `main` commit: `git tag v0.1.1 && git push origin v0.1.1`. CI checks
|
|
75
|
+
that the tag equals the version and builds every wheel and the sdist; the publish
|
|
76
|
+
job then waits for approval in the `pypi` environment (Actions → the run →
|
|
77
|
+
Review deployments) and uploads to PyPI.
|
|
62
78
|
|
|
63
79
|
A version uploaded to PyPI can be yanked (hidden from new installs) but its number can
|
|
64
80
|
never be reused. Fix mistakes with a new PATCH release.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastjsonpatch
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: Operating System :: OS Independent
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: Programming Language :: Rust
|
|
10
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
12
|
+
Requires-Dist: jsonpointer>=3.0,<4
|
|
13
|
+
Requires-Dist: pytest ; extra == 'test'
|
|
14
|
+
Requires-Dist: jsonpatch==1.34 ; extra == 'test'
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
License-File: THIRD_PARTY_LICENSES.md
|
|
18
|
+
Summary: Drop-in faster jsonpatch (RFC 6902): make_patch and apply in Rust
|
|
19
|
+
Keywords: json,patch,jsonpatch,rfc6902,diff,rust
|
|
20
|
+
Author-email: bitsbybyun@gmail.com
|
|
21
|
+
License-Expression: BSD-3-Clause
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
24
|
+
Project-URL: Changelog, https://github.com/amzntiger/fastjsonpatch/blob/main/CHANGELOG.md
|
|
25
|
+
Project-URL: Issues, https://github.com/amzntiger/fastjsonpatch/issues
|
|
26
|
+
Project-URL: Repository, https://github.com/amzntiger/fastjsonpatch
|
|
27
|
+
|
|
28
|
+
# fastjsonpatch
|
|
29
|
+
|
|
30
|
+
A drop-in replacement for [jsonpatch](https://github.com/stefankoegl/python-json-patch)
|
|
31
|
+
([RFC 6902](https://www.rfc-editor.org/rfc/rfc6902)) with `make_patch` and `apply`
|
|
32
|
+
written in Rust. It returns the same patches and documents as jsonpatch 1.34 and raises
|
|
33
|
+
exceptions with the same types and messages.
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pip install fastjsonpatch
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import fastjsonpatch as jsonpatch # the only line you change
|
|
41
|
+
|
|
42
|
+
src = {"name": "web", "replicas": 3, "ports": [80, 443]}
|
|
43
|
+
dst = {"name": "web", "replicas": 5, "ports": [80, 443, 8080]}
|
|
44
|
+
|
|
45
|
+
patch = jsonpatch.make_patch(src, dst)
|
|
46
|
+
patch.patch
|
|
47
|
+
# [{'op': 'replace', 'path': '/replicas', 'value': 5},
|
|
48
|
+
# {'op': 'add', 'path': '/ports/2', 'value': 8080}]
|
|
49
|
+
|
|
50
|
+
patch.apply(src) == dst # True; src is not modified
|
|
51
|
+
jsonpatch.apply_patch(src, [{"op": "remove", "path": "/ports/0"}])
|
|
52
|
+
# {'name': 'web', 'replicas': 3, 'ports': [443]}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Requires CPython 3.12+. Wheels are published for Linux (x86_64, aarch64; glibc and
|
|
56
|
+
musl), macOS (arm64, x86_64) and Windows (x64). The only dependency is `jsonpointer`.
|
|
57
|
+
|
|
58
|
+
## Performance
|
|
59
|
+
|
|
60
|
+
Linux x86_64, Python 3.13, best of several runs:
|
|
61
|
+
|
|
62
|
+
| workload | jsonpatch | fastjsonpatch | speedup |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `make_patch`, Kubernetes OpenAPI v1.33 → v1.34 (3.7 → 3.8 MB, 283 ops) | 136.4 ms | 9.6 ms | **14x** |
|
|
65
|
+
| `apply`, the same 283 ops | 78.0 ms | 11.4 ms | **7x** |
|
|
66
|
+
| `make_patch`, 10k records with 100 field edits | 292.61 ms | 5.26 ms | **56x** |
|
|
67
|
+
| `apply`, the same 100 ops | 89.38 ms | 7.90 ms | **11x** |
|
|
68
|
+
| `make_patch`, small REST resource ×1000 | 74.4 ms | 5.6 ms | **13x** |
|
|
69
|
+
| `apply_patch`, small REST resource ×1000 | 47.4 ms | 7.5 ms | **6x** |
|
|
70
|
+
|
|
71
|
+
Across all measured workloads, `make_patch` is 9-57x faster and `apply` 4-27x (4x is a
|
|
72
|
+
3-operation patch applied `in_place` to a 3.8 MB document, where no copy is made). On
|
|
73
|
+
Python 3.14, whose `copy.deepcopy` is faster, the `apply` speedup is smaller (about 6x
|
|
74
|
+
instead of 11x on the 10k-record workload). Full tables and an explanation of where
|
|
75
|
+
the time goes are in
|
|
76
|
+
[BENCHMARKS.md](https://github.com/amzntiger/fastjsonpatch/blob/main/BENCHMARKS.md);
|
|
77
|
+
`python bench/compare.py` (in the source distribution) measures on your machine.
|
|
78
|
+
|
|
79
|
+
## Compatibility
|
|
80
|
+
|
|
81
|
+
fastjsonpatch is jsonpatch 1.34's module with `JsonPatch.__init__` (validation),
|
|
82
|
+
`JsonPatch.from_diff` (`make_patch`) and `JsonPatch.apply` (`apply_patch`) routed to
|
|
83
|
+
Rust; every other public name is jsonpatch's own code. Inputs the Rust code does not
|
|
84
|
+
handle exactly fall back to jsonpatch's original code before anything is modified:
|
|
85
|
+
non-JSON values, dict or list subclasses, nesting deeper than 1000, a custom `dumps=`
|
|
86
|
+
or `json_dumper`, a custom `pointer_cls`, `operations` or `_get_operation` (for
|
|
87
|
+
`apply`), and operations that would fail.
|
|
88
|
+
|
|
89
|
+
Known differences:
|
|
90
|
+
|
|
91
|
+
- The exception classes are fastjsonpatch's own, so
|
|
92
|
+
`except fastjsonpatch.JsonPatchConflict` does not catch an exception raised by the `jsonpatch` package, and vice versa.
|
|
93
|
+
- Values in a document that are not dicts, lists or JSON scalars are copied with
|
|
94
|
+
`copy.deepcopy` one by one, so aliasing between such values is not preserved.
|
|
95
|
+
- `__version__` is fastjsonpatch's version; `__jsonpatch_version__` is `"1.34"`.
|
|
96
|
+
jsonpatch's `__author__` and `__website__` are not defined.
|
|
97
|
+
|
|
98
|
+
## Testing
|
|
99
|
+
|
|
100
|
+
About 1,580 tests: randomized comparisons against jsonpatch 1.34 (patches, documents
|
|
101
|
+
and errors), jsonpatch's own test suite and doctests, and the
|
|
102
|
+
[json-patch-tests](https://github.com/json-patch/json-patch-tests) RFC 6902 suite.
|
|
103
|
+
CI runs them with Python 3.12-3.14 on Linux (x86_64, aarch64; glibc), macOS (arm64,
|
|
104
|
+
x86_64) and Windows (x64); the musl wheels are built but not tested in CI.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
BSD-3-Clause. The Python module and the diff algorithm come from jsonpatch
|
|
109
|
+
(Copyright (c) 2011 Stefan Kögl, BSD). Licenses of the Rust crates compiled into the
|
|
110
|
+
wheels are in `THIRD_PARTY_LICENSES.md`. Development and release notes:
|
|
111
|
+
[MAINTAINING.md](https://github.com/amzntiger/fastjsonpatch/blob/main/MAINTAINING.md).
|
|
112
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# fastjsonpatch
|
|
2
|
+
|
|
3
|
+
A drop-in replacement for [jsonpatch](https://github.com/stefankoegl/python-json-patch)
|
|
4
|
+
([RFC 6902](https://www.rfc-editor.org/rfc/rfc6902)) with `make_patch` and `apply`
|
|
5
|
+
written in Rust. It returns the same patches and documents as jsonpatch 1.34 and raises
|
|
6
|
+
exceptions with the same types and messages.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
pip install fastjsonpatch
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import fastjsonpatch as jsonpatch # the only line you change
|
|
14
|
+
|
|
15
|
+
src = {"name": "web", "replicas": 3, "ports": [80, 443]}
|
|
16
|
+
dst = {"name": "web", "replicas": 5, "ports": [80, 443, 8080]}
|
|
17
|
+
|
|
18
|
+
patch = jsonpatch.make_patch(src, dst)
|
|
19
|
+
patch.patch
|
|
20
|
+
# [{'op': 'replace', 'path': '/replicas', 'value': 5},
|
|
21
|
+
# {'op': 'add', 'path': '/ports/2', 'value': 8080}]
|
|
22
|
+
|
|
23
|
+
patch.apply(src) == dst # True; src is not modified
|
|
24
|
+
jsonpatch.apply_patch(src, [{"op": "remove", "path": "/ports/0"}])
|
|
25
|
+
# {'name': 'web', 'replicas': 3, 'ports': [443]}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Requires CPython 3.12+. Wheels are published for Linux (x86_64, aarch64; glibc and
|
|
29
|
+
musl), macOS (arm64, x86_64) and Windows (x64). The only dependency is `jsonpointer`.
|
|
30
|
+
|
|
31
|
+
## Performance
|
|
32
|
+
|
|
33
|
+
Linux x86_64, Python 3.13, best of several runs:
|
|
34
|
+
|
|
35
|
+
| workload | jsonpatch | fastjsonpatch | speedup |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| `make_patch`, Kubernetes OpenAPI v1.33 → v1.34 (3.7 → 3.8 MB, 283 ops) | 136.4 ms | 9.6 ms | **14x** |
|
|
38
|
+
| `apply`, the same 283 ops | 78.0 ms | 11.4 ms | **7x** |
|
|
39
|
+
| `make_patch`, 10k records with 100 field edits | 292.61 ms | 5.26 ms | **56x** |
|
|
40
|
+
| `apply`, the same 100 ops | 89.38 ms | 7.90 ms | **11x** |
|
|
41
|
+
| `make_patch`, small REST resource ×1000 | 74.4 ms | 5.6 ms | **13x** |
|
|
42
|
+
| `apply_patch`, small REST resource ×1000 | 47.4 ms | 7.5 ms | **6x** |
|
|
43
|
+
|
|
44
|
+
Across all measured workloads, `make_patch` is 9-57x faster and `apply` 4-27x (4x is a
|
|
45
|
+
3-operation patch applied `in_place` to a 3.8 MB document, where no copy is made). On
|
|
46
|
+
Python 3.14, whose `copy.deepcopy` is faster, the `apply` speedup is smaller (about 6x
|
|
47
|
+
instead of 11x on the 10k-record workload). Full tables and an explanation of where
|
|
48
|
+
the time goes are in
|
|
49
|
+
[BENCHMARKS.md](https://github.com/amzntiger/fastjsonpatch/blob/main/BENCHMARKS.md);
|
|
50
|
+
`python bench/compare.py` (in the source distribution) measures on your machine.
|
|
51
|
+
|
|
52
|
+
## Compatibility
|
|
53
|
+
|
|
54
|
+
fastjsonpatch is jsonpatch 1.34's module with `JsonPatch.__init__` (validation),
|
|
55
|
+
`JsonPatch.from_diff` (`make_patch`) and `JsonPatch.apply` (`apply_patch`) routed to
|
|
56
|
+
Rust; every other public name is jsonpatch's own code. Inputs the Rust code does not
|
|
57
|
+
handle exactly fall back to jsonpatch's original code before anything is modified:
|
|
58
|
+
non-JSON values, dict or list subclasses, nesting deeper than 1000, a custom `dumps=`
|
|
59
|
+
or `json_dumper`, a custom `pointer_cls`, `operations` or `_get_operation` (for
|
|
60
|
+
`apply`), and operations that would fail.
|
|
61
|
+
|
|
62
|
+
Known differences:
|
|
63
|
+
|
|
64
|
+
- The exception classes are fastjsonpatch's own, so
|
|
65
|
+
`except fastjsonpatch.JsonPatchConflict` does not catch an exception raised by the `jsonpatch` package, and vice versa.
|
|
66
|
+
- Values in a document that are not dicts, lists or JSON scalars are copied with
|
|
67
|
+
`copy.deepcopy` one by one, so aliasing between such values is not preserved.
|
|
68
|
+
- `__version__` is fastjsonpatch's version; `__jsonpatch_version__` is `"1.34"`.
|
|
69
|
+
jsonpatch's `__author__` and `__website__` are not defined.
|
|
70
|
+
|
|
71
|
+
## Testing
|
|
72
|
+
|
|
73
|
+
About 1,580 tests: randomized comparisons against jsonpatch 1.34 (patches, documents
|
|
74
|
+
and errors), jsonpatch's own test suite and doctests, and the
|
|
75
|
+
[json-patch-tests](https://github.com/json-patch/json-patch-tests) RFC 6902 suite.
|
|
76
|
+
CI runs them with Python 3.12-3.14 on Linux (x86_64, aarch64; glibc), macOS (arm64,
|
|
77
|
+
x86_64) and Windows (x64); the musl wheels are built but not tested in CI.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
BSD-3-Clause. The Python module and the diff algorithm come from jsonpatch
|
|
82
|
+
(Copyright (c) 2011 Stefan Kögl, BSD). Licenses of the Rust crates compiled into the
|
|
83
|
+
wheels are in `THIRD_PARTY_LICENSES.md`. Development and release notes:
|
|
84
|
+
[MAINTAINING.md](https://github.com/amzntiger/fastjsonpatch/blob/main/MAINTAINING.md).
|
|
@@ -50,7 +50,6 @@ Custom pointer classes, custom ``operations`` and subclasses that override
|
|
|
50
50
|
import collections
|
|
51
51
|
import copy
|
|
52
52
|
import functools
|
|
53
|
-
import importlib.metadata
|
|
54
53
|
import json
|
|
55
54
|
from collections.abc import MutableMapping, MutableSequence, Sequence
|
|
56
55
|
from types import MappingProxyType
|
|
@@ -64,7 +63,7 @@ _ST_ADD = 0
|
|
|
64
63
|
_ST_REMOVE = 1
|
|
65
64
|
|
|
66
65
|
|
|
67
|
-
__version__ =
|
|
66
|
+
__version__ = _core.__version__
|
|
68
67
|
__license__ = 'BSD-3-Clause'
|
|
69
68
|
# Version of jsonpatch whose behaviour this module reproduces.
|
|
70
69
|
__jsonpatch_version__ = '1.34'
|
|
@@ -835,6 +835,9 @@ impl<'py> Applier<'_, 'py> {
|
|
|
835
835
|
if parts.is_empty() {
|
|
836
836
|
return Ok(Some(deepcopy(&v)?));
|
|
837
837
|
}
|
|
838
|
+
if parts.last().is_some_and(|p| p == "-") {
|
|
839
|
+
return Ok(None); // jsonpatch rejects "-" here even for a dict key
|
|
840
|
+
}
|
|
838
841
|
let Some(s) = slot(obj, parts)? else { return Ok(None) };
|
|
839
842
|
if slot_value(&s)?.is_none() {
|
|
840
843
|
return Ok(None);
|
|
@@ -962,5 +965,7 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
962
965
|
m.add_function(wrap_pyfunction!(apply, m)?)?;
|
|
963
966
|
m.add_function(wrap_pyfunction!(validate, m)?)?;
|
|
964
967
|
m.add_function(wrap_pyfunction!(py_deepcopy, m)?)?;
|
|
968
|
+
// The package version, so `fastjsonpatch.__version__` needs no importlib.metadata.
|
|
969
|
+
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
|
|
965
970
|
Ok(())
|
|
966
971
|
}
|
|
@@ -163,6 +163,25 @@ def test_invalid_patches(patch):
|
|
|
163
163
|
assert outcome(lambda: fastjsonpatch.JsonPatch(patch)) == outcome(lambda: jsonpatch.JsonPatch(patch))
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
@pytest.mark.parametrize("doc, patch", [
|
|
167
|
+
# jsonpatch rejects a final "-" in replace even when it names a dict key
|
|
168
|
+
({"-": 1}, [{"op": "replace", "path": "/-", "value": 2}]),
|
|
169
|
+
({"a": {"-": 1}}, [{"op": "replace", "path": "/a/-", "value": 2}]),
|
|
170
|
+
([1], [{"op": "replace", "path": "/-", "value": 2}]),
|
|
171
|
+
({"-": 1}, [{"op": "remove", "path": "/-"}]),
|
|
172
|
+
({"-": 1}, [{"op": "add", "path": "/-", "value": 2}]),
|
|
173
|
+
({"-": 1}, [{"op": "test", "path": "/-", "value": 1}]),
|
|
174
|
+
({"-": 1}, [{"op": "move", "from": "/-", "path": "/b"}]),
|
|
175
|
+
({"-": 1}, [{"op": "copy", "from": "/-", "path": "/b"}]),
|
|
176
|
+
])
|
|
177
|
+
def test_dash_key(doc, patch):
|
|
178
|
+
for in_place in (False, True):
|
|
179
|
+
a, b = copy.deepcopy(doc), copy.deepcopy(doc)
|
|
180
|
+
assert (outcome(lambda: fastjsonpatch.apply_patch(a, patch, in_place=in_place))
|
|
181
|
+
== outcome(lambda: jsonpatch.apply_patch(b, patch, in_place=in_place)))
|
|
182
|
+
assert repr(a) == repr(b)
|
|
183
|
+
|
|
184
|
+
|
|
166
185
|
def test_exceptions_have_the_same_hierarchy():
|
|
167
186
|
with pytest.raises(fastjsonpatch.JsonPatchConflict):
|
|
168
187
|
fastjsonpatch.apply_patch({}, [{"op": "remove", "path": "/a"}])
|
|
@@ -188,6 +207,41 @@ def test_standalone_does_not_import_jsonpatch():
|
|
|
188
207
|
assert out.stdout.strip() == "False"
|
|
189
208
|
|
|
190
209
|
|
|
210
|
+
def test_version_matches_the_installed_package():
|
|
211
|
+
# __version__ comes from Cargo.toml; the wheel's version from pyproject.toml.
|
|
212
|
+
import importlib.metadata
|
|
213
|
+
assert fastjsonpatch.__version__ == importlib.metadata.version("fastjsonpatch")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
_SPY_IMPORTLIB_METADATA = """
|
|
217
|
+
import sys, traceback
|
|
218
|
+
preloaded = "importlib.metadata" in sys.modules
|
|
219
|
+
for name in [m for m in sys.modules if m == "importlib.metadata" or m.startswith("importlib.metadata.")]:
|
|
220
|
+
del sys.modules[name]
|
|
221
|
+
importers = []
|
|
222
|
+
|
|
223
|
+
class Spy:
|
|
224
|
+
def find_spec(self, name, path=None, target=None):
|
|
225
|
+
if name == "importlib.metadata":
|
|
226
|
+
importers.append("".join(traceback.format_stack(limit=15)))
|
|
227
|
+
|
|
228
|
+
sys.meta_path.insert(0, Spy())
|
|
229
|
+
import fastjsonpatch
|
|
230
|
+
print("preloaded:", preloaded)
|
|
231
|
+
print(importers[0] if importers else "")
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_import_does_not_load_importlib_metadata():
|
|
236
|
+
# importlib.metadata alone used to add ~30 ms to `import fastjsonpatch`. Drop it
|
|
237
|
+
# from sys.modules first, in case the interpreter loaded it at startup, and
|
|
238
|
+
# record who imports it again.
|
|
239
|
+
out = subprocess.run([sys.executable, "-I", "-c", _SPY_IMPORTLIB_METADATA],
|
|
240
|
+
capture_output=True, text=True, check=True)
|
|
241
|
+
preloaded, _, importer = out.stdout.partition("\n")
|
|
242
|
+
assert not importer.strip(), f"{preloaded}; importlib.metadata imported by:\n{importer}"
|
|
243
|
+
|
|
244
|
+
|
|
191
245
|
def test_aliasing_preserved_like_deepcopy():
|
|
192
246
|
shared = [1, 2]
|
|
193
247
|
doc = {"a": shared, "b": shared}
|
fastjsonpatch-0.1.2/CHANGELOG.md
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.1.2 (2026-10-11)
|
|
4
|
-
|
|
5
|
-
Documentation only: the README now states only measured facts (including exactly
|
|
6
|
-
which platforms CI tests), and its link to MAINTAINING.md works on PyPI. No code
|
|
7
|
-
changes. (0.1.1 was tagged but not published.)
|
|
8
|
-
|
|
9
|
-
## 0.1.0 (2026-10-11)
|
|
10
|
-
|
|
11
|
-
First release. Behaves like jsonpatch 1.34 with `make_patch`, `apply`/`apply_patch`
|
|
12
|
-
and patch validation in Rust:
|
|
13
|
-
|
|
14
|
-
- `make_patch` 9-57x faster, `apply` 6-27x faster (see README).
|
|
15
|
-
- Standalone: depends only on `jsonpointer`.
|
|
16
|
-
- Wheels for Linux (glibc and musl, x86_64 and aarch64), macOS (arm64, x86_64) and
|
|
17
|
-
Windows (x64); one abi3 wheel per platform covers Python 3.12+.
|
fastjsonpatch-0.1.2/PKG-INFO
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: fastjsonpatch
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
Classifier: Development Status :: 4 - Beta
|
|
5
|
-
Classifier: Intended Audience :: Developers
|
|
6
|
-
Classifier: Operating System :: OS Independent
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
-
Classifier: Programming Language :: Rust
|
|
10
|
-
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
11
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
12
|
-
Requires-Dist: jsonpointer>=3.0,<4
|
|
13
|
-
Requires-Dist: pytest ; extra == 'test'
|
|
14
|
-
Requires-Dist: jsonpatch==1.34 ; extra == 'test'
|
|
15
|
-
Provides-Extra: test
|
|
16
|
-
License-File: LICENSE
|
|
17
|
-
License-File: THIRD_PARTY_LICENSES.md
|
|
18
|
-
Summary: Drop-in faster jsonpatch (RFC 6902): make_patch and apply in Rust
|
|
19
|
-
Keywords: json,patch,jsonpatch,rfc6902,diff,rust
|
|
20
|
-
Author-email: bitsbybyun@gmail.com
|
|
21
|
-
License-Expression: BSD-3-Clause
|
|
22
|
-
Requires-Python: >=3.12
|
|
23
|
-
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
24
|
-
Project-URL: Changelog, https://github.com/amzntiger/fastjsonpatch/blob/main/CHANGELOG.md
|
|
25
|
-
Project-URL: Issues, https://github.com/amzntiger/fastjsonpatch/issues
|
|
26
|
-
Project-URL: Repository, https://github.com/amzntiger/fastjsonpatch
|
|
27
|
-
|
|
28
|
-
# fastjsonpatch
|
|
29
|
-
|
|
30
|
-
A drop-in, faster [jsonpatch](https://github.com/stefankoegl/python-json-patch)
|
|
31
|
-
([RFC 6902](https://www.rfc-editor.org/rfc/rfc6902) JSON Patch) for Python.
|
|
32
|
-
`make_patch` and `apply` run in Rust and return the **same patches and the same
|
|
33
|
-
documents, and raise exceptions with the same types and messages,** as jsonpatch 1.34.
|
|
34
|
-
In the benchmarks below, making a patch is 9-57x faster and applying one 6-27x faster.
|
|
35
|
-
|
|
36
|
-
```sh
|
|
37
|
-
pip install fastjsonpatch
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
```python
|
|
41
|
-
import fastjsonpatch as jsonpatch # the only line you change
|
|
42
|
-
|
|
43
|
-
src = {"name": "web", "replicas": 3, "ports": [80, 443]}
|
|
44
|
-
dst = {"name": "web", "replicas": 5, "ports": [80, 443, 8080]}
|
|
45
|
-
|
|
46
|
-
patch = jsonpatch.make_patch(src, dst)
|
|
47
|
-
patch.patch
|
|
48
|
-
# [{'op': 'replace', 'path': '/replicas', 'value': 5},
|
|
49
|
-
# {'op': 'add', 'path': '/ports/2', 'value': 8080}]
|
|
50
|
-
|
|
51
|
-
patch.apply(src) == dst # True; src is not modified
|
|
52
|
-
jsonpatch.apply_patch(src, [{"op": "remove", "path": "/ports/0"}])
|
|
53
|
-
# {'name': 'web', 'replicas': 3, 'ports': [443]}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Python 3.12+ (CPython). Prebuilt wheels for Linux (x86_64 and aarch64, glibc and
|
|
57
|
-
musl), macOS (Apple Silicon and Intel) and Windows (x64); one wheel per platform covers
|
|
58
|
-
every Python from 3.12 on. The only dependency is `jsonpointer`.
|
|
59
|
-
|
|
60
|
-
## Why
|
|
61
|
-
|
|
62
|
-
JSON Patch describes a change to a JSON document as a list of operations (`add`,
|
|
63
|
-
`remove`, `replace`, `move`, `copy`, `test`). It is a media type for HTTP `PATCH`
|
|
64
|
-
requests (`application/json-patch+json`), Kubernetes accepts it
|
|
65
|
-
(`kubectl patch --type=json`), and it can store or send the difference between two
|
|
66
|
-
versions of a document.
|
|
67
|
-
|
|
68
|
-
jsonpatch is written in pure Python. In the measurements below:
|
|
69
|
-
|
|
70
|
-
- `make_patch` on a 10,000-record document takes 290 ms, and diffing two Kubernetes
|
|
71
|
-
OpenAPI specs (3.7 MB and 3.8 MB) takes 136 ms.
|
|
72
|
-
- `apply` deep-copies the whole document first, so a 3-operation patch on the
|
|
73
|
-
3.8 MB Kubernetes spec takes 71 ms, against 21 µs when applied in place.
|
|
74
|
-
- Applying or computing a patch on a small REST resource takes 47-74 µs per call.
|
|
75
|
-
|
|
76
|
-
fastjsonpatch keeps jsonpatch's behaviour exactly and moves these hot paths to Rust,
|
|
77
|
-
so existing code gets faster without changes.
|
|
78
|
-
|
|
79
|
-
## Speed
|
|
80
|
-
|
|
81
|
-
Measured on Linux x86_64, Python 3.13 (times are the best of several runs).
|
|
82
|
-
|
|
83
|
-
Real-world data:
|
|
84
|
-
|
|
85
|
-
| scenario | size | jsonpatch | fastjsonpatch | speedup |
|
|
86
|
-
|---|---|---|---|---|
|
|
87
|
-
| Kubernetes OpenAPI v1.33 (3.7 MB) → v1.34 (3.8 MB): make_patch | 283 ops | 136.4 ms | 9.6 ms | **14x** |
|
|
88
|
-
| Kubernetes OpenAPI v1.33 → v1.34: apply | 283 ops | 78.0 ms | 11.4 ms | **7x** |
|
|
89
|
-
| 3-op patch on the v1.34 spec | 3.8 MB | 71.1 ms | 10.1 ms | **7x** |
|
|
90
|
-
| 3-op patch on the v1.34 spec, `in_place=True` | 3.8 MB | 21.2 µs | 4.7 µs | **4x** |
|
|
91
|
-
| PyPI metadata snapshots (8k records): make_patch | 2,487 ops | 255.2 ms | 12.1 ms | **21x** |
|
|
92
|
-
| PyPI metadata snapshots: apply | 2,487 ops | 91.7 ms | 14.0 ms | **7x** |
|
|
93
|
-
| REST resource, apply_patch per request | ×1000 | 47.4 ms | 7.5 ms | **6x** |
|
|
94
|
-
| REST resource, make_patch per request | ×1000 | 74.4 ms | 5.6 ms | **13x** |
|
|
95
|
-
|
|
96
|
-
Synthetic records shaped like `{"id", "name", "score", "tags": [..], "meta": {..}}`:
|
|
97
|
-
|
|
98
|
-
| workload | ops | make_patch: jsonpatch | fastjsonpatch | speedup | apply: jsonpatch | fastjsonpatch | speedup |
|
|
99
|
-
|---|---|---|---|---|---|---|---|
|
|
100
|
-
| 10k records, 100 field edits | 100 | 292.61 ms | 5.26 ms | **56x** | 89.38 ms | 7.90 ms | **11x** |
|
|
101
|
-
| 10k records, insert 1 at front | 49911 | 669.77 ms | 42.78 ms | **16x** | 535.21 ms | 72.35 ms | **7x** |
|
|
102
|
-
| 10k records, delete 50 | 44900 | 636.53 ms | 40.46 ms | **16x** | 563.20 ms | 63.45 ms | **9x** |
|
|
103
|
-
| 10k records, move 20 | 26305 | 434.93 ms | 24.38 ms | **18x** | 325.64 ms | 41.03 ms | **8x** |
|
|
104
|
-
| 2k ints, insert 1 at front | 1 | 17.04 ms | 1.91 ms | **9x** | 581 µs | 21 µs | **27x** |
|
|
105
|
-
| small config (200 keys), 1 edit | 1 | 1.98 ms | 35 µs | **57x** | 464 µs | 32 µs | **15x** |
|
|
106
|
-
|
|
107
|
-
Python 3.14 made `copy.deepcopy` about 1.5x faster, so on 3.14 jsonpatch's `apply` on
|
|
108
|
-
a large document is faster too; on the "10k records, 100 field edits" workload the
|
|
109
|
-
`apply` speedup drops from about 11x to about 6x. The `make_patch` speedup stays about
|
|
110
|
-
the same. Run `python bench/compare.py` (from the source distribution) to measure on
|
|
111
|
-
your own machine.
|
|
112
|
-
|
|
113
|
-
## Why it is faster
|
|
114
|
-
|
|
115
|
-
**make_patch.** jsonpatch walks both documents in Python and compares every scalar
|
|
116
|
-
with `json.dumps(a) == json.dumps(b)`. Profiling the "10k records" workload with
|
|
117
|
-
cProfile puts about 40% of the time in `json.dumps` and about a third in `isinstance`
|
|
118
|
-
checks against abstract base classes (`MutableMapping`, `MutableSequence`).
|
|
119
|
-
fastjsonpatch walks the documents in Rust over the CPython objects directly (no
|
|
120
|
-
conversion to a Rust data structure), compares values natively with the same rules as the `json.dumps` comparison (`1`, `1.0` and `True`
|
|
121
|
-
are all different, `-0.0 != 0.0`, NaN equals NaN), keeps the current path on a stack,
|
|
122
|
-
and takes a single lockstep pass when two dicts have the same keys in the same order.
|
|
123
|
-
It is a port of jsonpatch's `DiffBuilder`, so it produces the same operations in the
|
|
124
|
-
same order, including detected moves.
|
|
125
|
-
|
|
126
|
-
**apply.** jsonpatch first runs `copy.deepcopy` on the document (so the original is
|
|
127
|
-
not modified), then builds a `JsonPointer` and an operation object for each operation.
|
|
128
|
-
For a small patch on a large document the copy is nearly all of the time.
|
|
129
|
-
fastjsonpatch copies each dict and list with one C call and only recurses into values
|
|
130
|
-
that are themselves containers; it keeps `deepcopy`'s semantics (shared and cyclic
|
|
131
|
-
references are copied once) but skips the bookkeeping for containers referenced only
|
|
132
|
-
once. It then resolves the paths and runs the operations without creating
|
|
133
|
-
`JsonPointer` or operation objects.
|
|
134
|
-
|
|
135
|
-
## Compatibility
|
|
136
|
-
|
|
137
|
-
fastjsonpatch is jsonpatch's own module (BSD licensed) with three methods routed to
|
|
138
|
-
Rust: `JsonPatch.__init__` (validation), `JsonPatch.from_diff` (`make_patch`) and
|
|
139
|
-
`JsonPatch.apply` (`apply_patch`). Everything else is jsonpatch's code, so the public
|
|
140
|
-
names are the same: `JsonPatch`, `make_patch`, `apply_patch`, `JsonPatch.from_string`,
|
|
141
|
-
the operation classes, `DiffBuilder`, `multidict`, and the exceptions
|
|
142
|
-
`JsonPatchException`, `InvalidJsonPatch`, `JsonPatchConflict`, `JsonPatchTestFailed`
|
|
143
|
-
(with the same hierarchy, and `JsonPointerException` from jsonpointer).
|
|
144
|
-
|
|
145
|
-
The Rust code handles the common case and hands anything else back to the original
|
|
146
|
-
Python code, so results never differ:
|
|
147
|
-
|
|
148
|
-
- **make_patch**: values it cannot reproduce exactly (non-JSON types that need
|
|
149
|
-
comparing, dict or list subclasses, a custom `dumps=`, nesting deeper than 1000)
|
|
150
|
-
are diffed by the original `DiffBuilder`.
|
|
151
|
-
- **apply**: works on plain dicts and lists. An operation that would fail, or that
|
|
152
|
-
touches anything else, is run by the original operation classes before anything is
|
|
153
|
-
mutated, so errors have the same type and message.
|
|
154
|
-
- `apply` and validation use the original code for a custom `pointer_cls`, a
|
|
155
|
-
subclass with custom `operations` or an overridden `_get_operation`, and patches
|
|
156
|
-
that are not lists or tuples. `make_patch` uses it for a custom `dumps=` or
|
|
157
|
-
`json_dumper`.
|
|
158
|
-
|
|
159
|
-
Known differences:
|
|
160
|
-
|
|
161
|
-
- The exception classes belong to fastjsonpatch. An exception raised by the original
|
|
162
|
-
`jsonpatch` package (for example inside another library that imports it) is not
|
|
163
|
-
caught by `except fastjsonpatch.JsonPatchConflict`, and vice versa.
|
|
164
|
-
- A value that is not a dict, list or JSON scalar inside a document being applied to
|
|
165
|
-
is copied with `copy.deepcopy` on its own, so aliasing between such values is not
|
|
166
|
-
preserved.
|
|
167
|
-
- `__version__` is fastjsonpatch's version, and jsonpatch's `__author__` and
|
|
168
|
-
`__website__` are not defined.
|
|
169
|
-
|
|
170
|
-
`fastjsonpatch.__jsonpatch_version__` says which jsonpatch release the behaviour
|
|
171
|
-
matches (currently `1.34`).
|
|
172
|
-
|
|
173
|
-
## How it is tested
|
|
174
|
-
|
|
175
|
-
About 1,570 tests. CI runs them with Python 3.12, 3.13 and 3.14 on Linux (x86_64 and
|
|
176
|
-
aarch64, glibc), macOS (arm64 and x86_64) and Windows (x64); the musl wheels are built
|
|
177
|
-
but not tested in CI:
|
|
178
|
-
|
|
179
|
-
- **Parity:** random documents, mutations and patches (valid and invalid) run through
|
|
180
|
-
both fastjsonpatch and jsonpatch 1.34, comparing the resulting patches and documents
|
|
181
|
-
by `repr` and errors by type and message. Also covers aliasing, cycles, non-JSON
|
|
182
|
-
values, subclasses, deep nesting and every fallback path.
|
|
183
|
-
- **jsonpatch's own test suite** and doctests, run against fastjsonpatch.
|
|
184
|
-
- **RFC 6902 conformance:** the
|
|
185
|
-
[json-patch-tests](https://github.com/json-patch/json-patch-tests) suite.
|
|
186
|
-
|
|
187
|
-
The source distribution includes the tests and the benchmark.
|
|
188
|
-
|
|
189
|
-
## Development
|
|
190
|
-
|
|
191
|
-
```sh
|
|
192
|
-
pip install maturin
|
|
193
|
-
maturin develop --release
|
|
194
|
-
pip install "jsonpatch==1.34" pytest # jsonpatch is the reference for the parity tests
|
|
195
|
-
pytest tests
|
|
196
|
-
python bench/compare.py # speed vs jsonpatch on this machine
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
See [MAINTAINING.md](https://github.com/amzntiger/fastjsonpatch/blob/main/MAINTAINING.md) for releases and upkeep.
|
|
200
|
-
|
|
201
|
-
## License
|
|
202
|
-
|
|
203
|
-
BSD-3-Clause. The Python module and the diff algorithm come from jsonpatch
|
|
204
|
-
(Copyright (c) 2011 Stefan Kögl, BSD). The licenses of the Rust crates compiled into
|
|
205
|
-
the wheels are in `THIRD_PARTY_LICENSES.md`.
|
|
206
|
-
|
fastjsonpatch-0.1.2/README.md
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
# fastjsonpatch
|
|
2
|
-
|
|
3
|
-
A drop-in, faster [jsonpatch](https://github.com/stefankoegl/python-json-patch)
|
|
4
|
-
([RFC 6902](https://www.rfc-editor.org/rfc/rfc6902) JSON Patch) for Python.
|
|
5
|
-
`make_patch` and `apply` run in Rust and return the **same patches and the same
|
|
6
|
-
documents, and raise exceptions with the same types and messages,** as jsonpatch 1.34.
|
|
7
|
-
In the benchmarks below, making a patch is 9-57x faster and applying one 6-27x faster.
|
|
8
|
-
|
|
9
|
-
```sh
|
|
10
|
-
pip install fastjsonpatch
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
```python
|
|
14
|
-
import fastjsonpatch as jsonpatch # the only line you change
|
|
15
|
-
|
|
16
|
-
src = {"name": "web", "replicas": 3, "ports": [80, 443]}
|
|
17
|
-
dst = {"name": "web", "replicas": 5, "ports": [80, 443, 8080]}
|
|
18
|
-
|
|
19
|
-
patch = jsonpatch.make_patch(src, dst)
|
|
20
|
-
patch.patch
|
|
21
|
-
# [{'op': 'replace', 'path': '/replicas', 'value': 5},
|
|
22
|
-
# {'op': 'add', 'path': '/ports/2', 'value': 8080}]
|
|
23
|
-
|
|
24
|
-
patch.apply(src) == dst # True; src is not modified
|
|
25
|
-
jsonpatch.apply_patch(src, [{"op": "remove", "path": "/ports/0"}])
|
|
26
|
-
# {'name': 'web', 'replicas': 3, 'ports': [443]}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Python 3.12+ (CPython). Prebuilt wheels for Linux (x86_64 and aarch64, glibc and
|
|
30
|
-
musl), macOS (Apple Silicon and Intel) and Windows (x64); one wheel per platform covers
|
|
31
|
-
every Python from 3.12 on. The only dependency is `jsonpointer`.
|
|
32
|
-
|
|
33
|
-
## Why
|
|
34
|
-
|
|
35
|
-
JSON Patch describes a change to a JSON document as a list of operations (`add`,
|
|
36
|
-
`remove`, `replace`, `move`, `copy`, `test`). It is a media type for HTTP `PATCH`
|
|
37
|
-
requests (`application/json-patch+json`), Kubernetes accepts it
|
|
38
|
-
(`kubectl patch --type=json`), and it can store or send the difference between two
|
|
39
|
-
versions of a document.
|
|
40
|
-
|
|
41
|
-
jsonpatch is written in pure Python. In the measurements below:
|
|
42
|
-
|
|
43
|
-
- `make_patch` on a 10,000-record document takes 290 ms, and diffing two Kubernetes
|
|
44
|
-
OpenAPI specs (3.7 MB and 3.8 MB) takes 136 ms.
|
|
45
|
-
- `apply` deep-copies the whole document first, so a 3-operation patch on the
|
|
46
|
-
3.8 MB Kubernetes spec takes 71 ms, against 21 µs when applied in place.
|
|
47
|
-
- Applying or computing a patch on a small REST resource takes 47-74 µs per call.
|
|
48
|
-
|
|
49
|
-
fastjsonpatch keeps jsonpatch's behaviour exactly and moves these hot paths to Rust,
|
|
50
|
-
so existing code gets faster without changes.
|
|
51
|
-
|
|
52
|
-
## Speed
|
|
53
|
-
|
|
54
|
-
Measured on Linux x86_64, Python 3.13 (times are the best of several runs).
|
|
55
|
-
|
|
56
|
-
Real-world data:
|
|
57
|
-
|
|
58
|
-
| scenario | size | jsonpatch | fastjsonpatch | speedup |
|
|
59
|
-
|---|---|---|---|---|
|
|
60
|
-
| Kubernetes OpenAPI v1.33 (3.7 MB) → v1.34 (3.8 MB): make_patch | 283 ops | 136.4 ms | 9.6 ms | **14x** |
|
|
61
|
-
| Kubernetes OpenAPI v1.33 → v1.34: apply | 283 ops | 78.0 ms | 11.4 ms | **7x** |
|
|
62
|
-
| 3-op patch on the v1.34 spec | 3.8 MB | 71.1 ms | 10.1 ms | **7x** |
|
|
63
|
-
| 3-op patch on the v1.34 spec, `in_place=True` | 3.8 MB | 21.2 µs | 4.7 µs | **4x** |
|
|
64
|
-
| PyPI metadata snapshots (8k records): make_patch | 2,487 ops | 255.2 ms | 12.1 ms | **21x** |
|
|
65
|
-
| PyPI metadata snapshots: apply | 2,487 ops | 91.7 ms | 14.0 ms | **7x** |
|
|
66
|
-
| REST resource, apply_patch per request | ×1000 | 47.4 ms | 7.5 ms | **6x** |
|
|
67
|
-
| REST resource, make_patch per request | ×1000 | 74.4 ms | 5.6 ms | **13x** |
|
|
68
|
-
|
|
69
|
-
Synthetic records shaped like `{"id", "name", "score", "tags": [..], "meta": {..}}`:
|
|
70
|
-
|
|
71
|
-
| workload | ops | make_patch: jsonpatch | fastjsonpatch | speedup | apply: jsonpatch | fastjsonpatch | speedup |
|
|
72
|
-
|---|---|---|---|---|---|---|---|
|
|
73
|
-
| 10k records, 100 field edits | 100 | 292.61 ms | 5.26 ms | **56x** | 89.38 ms | 7.90 ms | **11x** |
|
|
74
|
-
| 10k records, insert 1 at front | 49911 | 669.77 ms | 42.78 ms | **16x** | 535.21 ms | 72.35 ms | **7x** |
|
|
75
|
-
| 10k records, delete 50 | 44900 | 636.53 ms | 40.46 ms | **16x** | 563.20 ms | 63.45 ms | **9x** |
|
|
76
|
-
| 10k records, move 20 | 26305 | 434.93 ms | 24.38 ms | **18x** | 325.64 ms | 41.03 ms | **8x** |
|
|
77
|
-
| 2k ints, insert 1 at front | 1 | 17.04 ms | 1.91 ms | **9x** | 581 µs | 21 µs | **27x** |
|
|
78
|
-
| small config (200 keys), 1 edit | 1 | 1.98 ms | 35 µs | **57x** | 464 µs | 32 µs | **15x** |
|
|
79
|
-
|
|
80
|
-
Python 3.14 made `copy.deepcopy` about 1.5x faster, so on 3.14 jsonpatch's `apply` on
|
|
81
|
-
a large document is faster too; on the "10k records, 100 field edits" workload the
|
|
82
|
-
`apply` speedup drops from about 11x to about 6x. The `make_patch` speedup stays about
|
|
83
|
-
the same. Run `python bench/compare.py` (from the source distribution) to measure on
|
|
84
|
-
your own machine.
|
|
85
|
-
|
|
86
|
-
## Why it is faster
|
|
87
|
-
|
|
88
|
-
**make_patch.** jsonpatch walks both documents in Python and compares every scalar
|
|
89
|
-
with `json.dumps(a) == json.dumps(b)`. Profiling the "10k records" workload with
|
|
90
|
-
cProfile puts about 40% of the time in `json.dumps` and about a third in `isinstance`
|
|
91
|
-
checks against abstract base classes (`MutableMapping`, `MutableSequence`).
|
|
92
|
-
fastjsonpatch walks the documents in Rust over the CPython objects directly (no
|
|
93
|
-
conversion to a Rust data structure), compares values natively with the same rules as the `json.dumps` comparison (`1`, `1.0` and `True`
|
|
94
|
-
are all different, `-0.0 != 0.0`, NaN equals NaN), keeps the current path on a stack,
|
|
95
|
-
and takes a single lockstep pass when two dicts have the same keys in the same order.
|
|
96
|
-
It is a port of jsonpatch's `DiffBuilder`, so it produces the same operations in the
|
|
97
|
-
same order, including detected moves.
|
|
98
|
-
|
|
99
|
-
**apply.** jsonpatch first runs `copy.deepcopy` on the document (so the original is
|
|
100
|
-
not modified), then builds a `JsonPointer` and an operation object for each operation.
|
|
101
|
-
For a small patch on a large document the copy is nearly all of the time.
|
|
102
|
-
fastjsonpatch copies each dict and list with one C call and only recurses into values
|
|
103
|
-
that are themselves containers; it keeps `deepcopy`'s semantics (shared and cyclic
|
|
104
|
-
references are copied once) but skips the bookkeeping for containers referenced only
|
|
105
|
-
once. It then resolves the paths and runs the operations without creating
|
|
106
|
-
`JsonPointer` or operation objects.
|
|
107
|
-
|
|
108
|
-
## Compatibility
|
|
109
|
-
|
|
110
|
-
fastjsonpatch is jsonpatch's own module (BSD licensed) with three methods routed to
|
|
111
|
-
Rust: `JsonPatch.__init__` (validation), `JsonPatch.from_diff` (`make_patch`) and
|
|
112
|
-
`JsonPatch.apply` (`apply_patch`). Everything else is jsonpatch's code, so the public
|
|
113
|
-
names are the same: `JsonPatch`, `make_patch`, `apply_patch`, `JsonPatch.from_string`,
|
|
114
|
-
the operation classes, `DiffBuilder`, `multidict`, and the exceptions
|
|
115
|
-
`JsonPatchException`, `InvalidJsonPatch`, `JsonPatchConflict`, `JsonPatchTestFailed`
|
|
116
|
-
(with the same hierarchy, and `JsonPointerException` from jsonpointer).
|
|
117
|
-
|
|
118
|
-
The Rust code handles the common case and hands anything else back to the original
|
|
119
|
-
Python code, so results never differ:
|
|
120
|
-
|
|
121
|
-
- **make_patch**: values it cannot reproduce exactly (non-JSON types that need
|
|
122
|
-
comparing, dict or list subclasses, a custom `dumps=`, nesting deeper than 1000)
|
|
123
|
-
are diffed by the original `DiffBuilder`.
|
|
124
|
-
- **apply**: works on plain dicts and lists. An operation that would fail, or that
|
|
125
|
-
touches anything else, is run by the original operation classes before anything is
|
|
126
|
-
mutated, so errors have the same type and message.
|
|
127
|
-
- `apply` and validation use the original code for a custom `pointer_cls`, a
|
|
128
|
-
subclass with custom `operations` or an overridden `_get_operation`, and patches
|
|
129
|
-
that are not lists or tuples. `make_patch` uses it for a custom `dumps=` or
|
|
130
|
-
`json_dumper`.
|
|
131
|
-
|
|
132
|
-
Known differences:
|
|
133
|
-
|
|
134
|
-
- The exception classes belong to fastjsonpatch. An exception raised by the original
|
|
135
|
-
`jsonpatch` package (for example inside another library that imports it) is not
|
|
136
|
-
caught by `except fastjsonpatch.JsonPatchConflict`, and vice versa.
|
|
137
|
-
- A value that is not a dict, list or JSON scalar inside a document being applied to
|
|
138
|
-
is copied with `copy.deepcopy` on its own, so aliasing between such values is not
|
|
139
|
-
preserved.
|
|
140
|
-
- `__version__` is fastjsonpatch's version, and jsonpatch's `__author__` and
|
|
141
|
-
`__website__` are not defined.
|
|
142
|
-
|
|
143
|
-
`fastjsonpatch.__jsonpatch_version__` says which jsonpatch release the behaviour
|
|
144
|
-
matches (currently `1.34`).
|
|
145
|
-
|
|
146
|
-
## How it is tested
|
|
147
|
-
|
|
148
|
-
About 1,570 tests. CI runs them with Python 3.12, 3.13 and 3.14 on Linux (x86_64 and
|
|
149
|
-
aarch64, glibc), macOS (arm64 and x86_64) and Windows (x64); the musl wheels are built
|
|
150
|
-
but not tested in CI:
|
|
151
|
-
|
|
152
|
-
- **Parity:** random documents, mutations and patches (valid and invalid) run through
|
|
153
|
-
both fastjsonpatch and jsonpatch 1.34, comparing the resulting patches and documents
|
|
154
|
-
by `repr` and errors by type and message. Also covers aliasing, cycles, non-JSON
|
|
155
|
-
values, subclasses, deep nesting and every fallback path.
|
|
156
|
-
- **jsonpatch's own test suite** and doctests, run against fastjsonpatch.
|
|
157
|
-
- **RFC 6902 conformance:** the
|
|
158
|
-
[json-patch-tests](https://github.com/json-patch/json-patch-tests) suite.
|
|
159
|
-
|
|
160
|
-
The source distribution includes the tests and the benchmark.
|
|
161
|
-
|
|
162
|
-
## Development
|
|
163
|
-
|
|
164
|
-
```sh
|
|
165
|
-
pip install maturin
|
|
166
|
-
maturin develop --release
|
|
167
|
-
pip install "jsonpatch==1.34" pytest # jsonpatch is the reference for the parity tests
|
|
168
|
-
pytest tests
|
|
169
|
-
python bench/compare.py # speed vs jsonpatch on this machine
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
See [MAINTAINING.md](https://github.com/amzntiger/fastjsonpatch/blob/main/MAINTAINING.md) for releases and upkeep.
|
|
173
|
-
|
|
174
|
-
## License
|
|
175
|
-
|
|
176
|
-
BSD-3-Clause. The Python module and the diff algorithm come from jsonpatch
|
|
177
|
-
(Copyright (c) 2011 Stefan Kögl, BSD). The licenses of the Rust crates compiled into
|
|
178
|
-
the wheels are in `THIRD_PARTY_LICENSES.md`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|