Metadata-Version: 2.4
Name: json-source-edit
Version: 0.3.1
Summary: Surgical, byte-preserving JSON editing — change only what you intend to change.
Author: Jérémie Lumbroso
License: MIT
Project-URL: Homepage, https://github.com/jlumbroso/json-source-edit
Project-URL: Repository, https://github.com/jlumbroso/json-source-edit
Project-URL: Provenance, https://github.com/jlumbroso/json-source-edit/blob/main/PROVENANCE.md
Keywords: json,json-patch,diff,text-editing,format-preserving
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Markup
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: json-source-map>=1.0.5
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Provides-Extra: docs
Requires-Dist: mkdocs<2.0,>=1.6; extra == "docs"
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
Dynamic: license-file

# json-source-edit

A Python package for **surgical, byte-preserving JSON editing.** Change
only what you intend to change — everything else in the file survives
byte-for-byte, so your diff shows your edit, not a full-file rewrite.

## The problem

The obvious way to edit a JSON file is parse → modify → serialize:

```python
data = json.loads(text)
data["steps"][1]["line"] = 433
text = json.dumps(data, indent=2)
```

This is correct, and it destroys your diff. Real evidence, from the project
this package was extracted out of: three simple changes to one file — two
field updates and one deletion — were *expected* to produce a ~10-line diff.
The parse-modify-serialize round-trip produced a **500+ line diff** instead.

That's because `json.dumps` doesn't know or care what your original
formatting was — indentation, key order, quote style, all of it gets
re-decided from scratch, every time, whether or not you touched it.
Multiply that by a codebase-sized JSON file and code review becomes
impossible: every line looks changed, and the actual edit is buried.

`json-source-edit` fixes this at the source: it never re-serializes the
document. It computes exact byte positions for each change against the
*original text* and splices replacement bytes into the gaps — the same
operation a film or tape splicer performs, cut and rejoin at an exact point,
everything else on the reel undisturbed.

## Install

```bash
pip install json-source-edit
```

(Or clone the repo and `pip install -e .` for local development.)

## Usage

```python
from json_source_edit import JSONEditor

editor = JSONEditor.from_string("""{
  "title": "Rust Basics",
  "steps": [
    {"file": "src/main.rs", "line": 10},
    {"file": "src/lib.rs", "line": 20}
  ]
}""")
editor.replace("/steps/0/line", 433)
editor.remove("/steps/1")
result = editor.apply(validate=True)
print(editor.preview_diff())
```

```diff
--- original
+++ modified
@@ -1,7 +1,6 @@
 {
   "title": "Rust Basics",
   "steps": [
-    {"file": "src/main.rs", "line": 10},
-    {"file": "src/lib.rs", "line": 20}
+    {"file": "src/main.rs", "line": 433}
   ]
 }
```

`validate=True` catches mistakes before they reach you. It reconstructs the
expected result independently — replaying your edits against a copy of the
parsed document — and diffs that against what the surgical edit actually
produced. If they disagree, it raises `SemanticValidationError` instead of
silently returning something subtly wrong.

`apply()` defaults to `validate=False`; `save()` defaults to `validate=True`
— writing to disk is where a mistake actually costs you something, so that
path checks unless you opt out.

Three more methods help you inspect a pending batch before committing to
it: `get_value`, `get_modifications`, and `preview_diff`.

## Scope

| JSON Patch operation | Status |
|---|---|
| `replace` | Supported — any path, any depth |
| `remove` | Supported — array elements and object properties, any depth, except the document root itself (structurally undefined: the root has no parent container to apply a comma/whitespace rule against) |
| `test` | Supported — not a text edit, an assertion. Evaluated against the document as it stood before this batch's own operations (not a naive sequential reading of RFC 6902 — see the docstring on `operations.Test`), with type-strict comparison (`1` does not test-equal `1.0` or `true`) |
| `add` | Supported — new array index (any position, or `-` to append) or object key. An *existing* object key is replaced instead, per RFC 6902 — an existing array *index* is always an insert, never a replace (see `docs/adr/0002-*.md`). The document root is out of scope, same posture as `remove`. |
| `move` | Supported — RFC 6902 §4.4, a `remove` composed with an `add`. Rejects moving a location into one of its own children, and moving a location to itself. |
| `copy` | Supported — RFC 6902 §4.5, an `add` using a value resolved (and deep-copied) from elsewhere in the document. |

Every JSON Patch operation is implemented. Multiple `replace`/`remove`/
`test`/`add`/`move`/`copy` calls batch correctly against the same original
document — every path resolves in original-document coordinates
regardless of what else is in the batch, so edit order doesn't matter and
one deletion or insertion can't corrupt another edit's position.

## Benchmark (v0)

Reproduce with `python benchmarks/throughput.py` — a standalone script, no
test framework required.

Methodology: replaces 5% of a flat array's elements at each size, median of
5 runs, reporting throughput against the *original* document size (the more
relevant number for "can this handle my file" than edit count alone).

Single-machine numbers, not a controlled benchmark environment — expect
real variance run to run and machine to machine; rerun locally before
relying on any of this for capacity planning.

| Size | Elements | Document | Edits | Median time | Throughput |
|---|---|---|---|---|---|
| small | 200 | 0.01MB | 10 | 0.65ms | 15,353 edits/sec, 13.7 MB/sec |
| medium | 2,000 | 0.09MB | 100 | 6.25ms | 15,990 edits/sec, 14.9 MB/sec |
| large | 20,000 | 0.97MB | 1000 | 87.22ms | 11,465 edits/sec, 11.1 MB/sec |

(Measured on macOS/arm64, Python 3.12.4, 2026-08-01 — see the script's own
output for full per-run samples; the "large" row in particular showed
bimodal timing on this machine, a real observation, not smoothed over.)

## Provenance

This package was designed and hardened inside
[`codetour-cli`](https://github.com/jlumbroso/codetour-cli), across four
independent review gates, before being extracted here with no known
remaining incompleteness. See [`PROVENANCE.md`](PROVENANCE.md) for the
commit-by-commit history and [`docs/adr/0001-extraction-from-codetour-cli.md`](docs/adr/0001-extraction-from-codetour-cli.md)
for the extraction decisions themselves (import strategy, the dependency
verdict, the supported-Python-versions floor).

## Credits

The position-mapping this package edits against — turning a JSON document
into exact byte offsets for every value and key — is done by
[`json-source-map`](https://pypi.org/project/json-source-map/), a small,
dependency-free library by David Andersson. It's the one piece of this
package that isn't ours: everything in `src/json_source_edit/source_map.py`
is a thin seam around it, and everything else in this package builds on top
of what it computes. Elegant, narrowly-scoped libraries like this are what
make a package like this one feasible to write at all.

## Status

Published: [pypi.org/project/json-source-edit](https://pypi.org/project/json-source-edit/).
`pip install json-source-edit`. 243 tests, 100% line coverage, full JSON
Patch operation coverage. See `CHANGELOG.md` for release history. Design
decisions beyond the original extraction live in `docs/adr/` (`0002`:
`add`'s insertion doctrine, including the `move`/`copy` compositions;
`0003`: `test`'s pristine-batch semantics).

## License

MIT.
