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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

**Bidirectional, JSON-patch-compliant diffs between Python data structures.**

📖 [Documentation](https://fork-tongue.github.io/patchdiff/) [Quick Start](https://fork-tongue.github.io/patchdiff/getting-started/quick-start/) [API Reference](https://fork-tongue.github.io/patchdiff/reference/api/)
📖 [Documentation](https://fork-tongue.github.io/patchdiff/) | [Quick Start](https://fork-tongue.github.io/patchdiff/getting-started/quick-start/) | [API Reference](https://fork-tongue.github.io/patchdiff/reference/api/)

Patchdiff diffs composite structures of dicts, lists, sets and tuples, and gives you **both directions** in one call: the patches to get from `input` to `output`, and the patches to get back. Patches are [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) JSON-patch style, serializable to JSON, and can be applied in place or to a copy which makes undo/redo, change synchronization and state auditing one-liners.
Patchdiff diffs composite structures of dicts, lists, sets and tuples, and gives you **both directions** in one call: the patches to get from `input` to `output`, and the patches to get back. Patches are [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) JSON-patch style, serializable to JSON, and can be applied in place or to a copy, which makes undo/redo, change synchronization and state auditing one-liners.

```python
from patchdiff import apply, diff, iapply, to_json
Expand All @@ -26,7 +26,7 @@ assert input == output
print(to_json(ops, indent=4))
```

## Don't diff — record
## Proxy based patch generation

When your own code makes the changes, `produce()` (inspired by [Immer](https://immerjs.github.io/immer/produce)) skips the comparison entirely: it hands your recipe a draft, records every mutation, and returns the result plus both patch directions. Cost scales with the number of mutations instead of the size of the state:

Expand All @@ -45,7 +45,7 @@ assert base == {"count": 0, "items": [1, 2, 3]} # base is untouched
assert result == {"count": 5, "items": [1, 2, 3, 4]}
```

With `in_place=True`, mutations (and patches applied with `iapply`) write straight through proxy-backed state the natural companion to [observ](https://github.com/fork-tongue/observ) reactive objects, where mutating through the proxy is what triggers watchers. See [Observ Integration](https://fork-tongue.github.io/patchdiff/guide/observ/) for reactive state with undo/redo.
With `in_place=True`, mutations (and patches applied with `iapply`) write straight through proxy-backed state, the natural companion to [observ](https://github.com/fork-tongue/observ) reactive objects, where mutating through the proxy is what triggers watchers. See [Observ Integration](https://fork-tongue.github.io/patchdiff/guide/observ/) for reactive state with undo/redo.

## Install

Expand Down
179 changes: 0 additions & 179 deletions ROADMAP.md

This file was deleted.

6 changes: 4 additions & 2 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Installation

Patchdiff is published on [PyPI](https://pypi.org/project/patchdiff/) and has **no dependencies**. It requires Python 3.9 or newer.
Patchdiff is published on [PyPI](https://pypi.org/project/patchdiff/):

```sh
pip install patchdiff
Expand All @@ -12,4 +12,6 @@ Or with [uv](https://docs.astral.sh/uv/):
uv add patchdiff
```

That's it — head over to the [Quick Start](quick-start.md).
There are no dependencies. Python 3.9 or newer is required.

Next up: the [quick start](quick-start.md).
22 changes: 11 additions & 11 deletions docs/getting-started/quick-start.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Quick Start

This page walks through the whole API in a few minutes: diff two structures, apply the patches, undo them, and serialize them to JSON.
This page walks through the whole API: diff two objects, apply the patches, undo them, and serialize them to JSON.

## Diff two structures
## Diffing

[`diff`][patchdiff.diff.diff] compares two objects and returns two lists of operations: one that turns `input` into `output`, and one that turns `output` back into `input`.

Expand All @@ -15,7 +15,7 @@ output = {"a": [5, 2, 9, {"b", "c"}], "b": 6, "c": 7}
ops, reverse_ops = diff(input, output)
```

Each operation is a plain dict in JSON patch style an `"op"` (`"add"`, `"remove"` or `"replace"`), a `"path"` (a [`Pointer`][patchdiff.pointer.Pointer]), and a `"value"` for adds and replaces:
Each operation is a plain dict in JSON patch style, with an `"op"` (`"add"`, `"remove"` or `"replace"`), a `"path"` (a [`Pointer`][patchdiff.pointer.Pointer]), and a `"value"` for adds and replaces:

```python
from patchdiff import diff
Expand All @@ -27,9 +27,9 @@ assert ops == [{"op": "replace", "path": Pointer(["count"]), "value": 1}]
assert reverse_ops == [{"op": "replace", "path": Pointer(["count"]), "value": 0}]
```

## Apply patches
## Applying patches

[`apply`][patchdiff.apply.apply] patches a **deep copy** and leaves the original untouched; [`iapply`][patchdiff.apply.iapply] patches the object **in place**:
[`apply`][patchdiff.apply.apply] patches a **deep copy** and leaves the original untouched, while [`iapply`][patchdiff.apply.iapply] patches the object **in place**:

```python
from patchdiff import apply, diff, iapply
Expand All @@ -46,11 +46,11 @@ iapply(input, ops) # in-place
assert input == output
```

Applying `reverse_ops` is your undo; re-applying `ops` is your redo.
Applying `reverse_ops` is your undo, re-applying `ops` is your redo.

## Serialize to JSON
## Serializing to JSON

Patches are JSON-patch compliant, so they can be serialized with [`to_json`][patchdiff.serialize.to_json] and shipped anywhere:
Patches are JSON-patch compliant, so they can be serialized with [`to_json`][patchdiff.serialize.to_json]:

```python
from patchdiff import diff, to_json
Expand All @@ -75,9 +75,9 @@ print(to_json(ops, indent=4))
]
```

## Record patches while mutating
## Recording patches while mutating

When you're the one making the changes, you don't need to diff at all: [`produce`][patchdiff.produce.produce] hands you a draft, records every mutation you make to it, and gives you the result plus both patch directions without a full comparison pass:
When your own code is making the changes, you don't need to diff at all. [`produce`][patchdiff.produce.produce] hands you a draft, records every mutation you make to it, and gives you the result plus both patch directions, without a full comparison pass:

```python
from patchdiff import produce
Expand All @@ -95,4 +95,4 @@ assert base == {"count": 0, "items": [1, 2, 3]} # untouched
assert result == {"count": 5, "items": [1, 2, 3, 4], "new_field": "hello"}
```

Read on in the [Guide](../guide/diffing.md) for the details of each of these, or jump to the [API Reference](../reference/api.md).
The [guide](../guide/diffing.md) goes into the details of each of these. The complete public API is in the [API reference](../reference/api.md).
8 changes: 4 additions & 4 deletions docs/guide/applying.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Patchdiff can apply a list of operations to an object in two ways:

* [`apply`][patchdiff.apply.apply] first makes a **deep copy**, patches that, and returns it — the input is left untouched.
* [`apply`][patchdiff.apply.apply] first makes a **deep copy**, patches that, and returns it. The input is left untouched.
* [`iapply`][patchdiff.apply.iapply] patches the object **in place** and returns the same object. This is faster (no copy) and is what you want for objects that must keep their identity, such as observ reactive proxies.

```python
Expand All @@ -24,11 +24,11 @@ assert input == output

## Order matters

Operations are applied sequentially, and paths refer to the state of the object *at that point in the sequence* — exactly like RFC 6902. List indices in particular shift as adds and removes are applied, and both lists that [`diff`][patchdiff.diff.diff] returns are already ordered accordingly. Apply them as-is; don't reorder or cherry-pick individual operations.
Operations are applied sequentially, and paths refer to the state of the object at that point in the sequence, just like in RFC 6902. List indices in particular shift as adds and removes are applied. Both lists that [`diff`][patchdiff.diff.diff] returns are already ordered accordingly, so apply them as-is. Don't reorder or cherry-pick individual operations.

## Undo and redo

Because every diff comes with its reverse, undo/redo is just a pair of stacks of patch lists:
Since every diff comes with its reverse, undo/redo is just a pair of stacks of patch lists:

```python
from patchdiff import diff, iapply
Expand Down Expand Up @@ -63,7 +63,7 @@ assert state == {"count": 2}

## Patch values are copied on write

When a patch is applied, its `"value"` is **deep-copied** before being written into the target. The patched object and the patch list therefore never share mutable state you can keep patches around (say, on an undo stack) and freely mutate the object afterwards:
When a patch is applied, its `"value"` is **deep-copied** before being written into the target. The patched object and the patch list never share mutable state, so you can keep patches around (on an undo stack for example) and freely mutate the object afterwards:

```python
from patchdiff import diff, iapply
Expand Down
Loading