Skip to content

Commit a1de471

Browse files
Add produce() function for proxy-based patch generation (issue #8)
Introduce `produce()` as a new public API that uses proxy objects (DictProxy, ListProxy, SetProxy) to track mutations in real-time and emit JSON patches as changes occur — an alternative to post-hoc diff(). - Add patchdiff/produce.py with proxy classes and PatchRecorder - Support optional observ reactive objects via to_raw() integration - Add in_place option to mutate the original object directly - Fix Pointer.__eq__ type hint and modernize tuple return type hint - Expand benchmarks with produce() vs diff() and observ comparisons - Add comprehensive tests for dict, list, set, core, and observ integration - Update README with produce() usage examples
1 parent f43eac6 commit a1de471

13 files changed

Lines changed: 4426 additions & 8 deletions

.github/workflows/benchmark.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
python-version: "3.14"
2222

2323
- name: Install dependencies
24-
run: uv sync
24+
run: uv sync --group observ
2525

2626
# On PRs: run benchmarks twice (PR code vs master code) and compare
2727
- name: Run benchmarks

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
with:
4242
python-version: ${{ matrix.pyversion }}
4343
- name: Install dependencies
44-
run: uv sync
44+
run: uv sync --group observ
4545
- name: Lint
4646
run: uv run ruff check
4747
- name: Format
@@ -61,7 +61,7 @@ jobs:
6161
with:
6262
python-version: "3.9"
6363
- name: Install dependencies
64-
run: uv sync
64+
run: uv sync --group observ
6565
- name: Build wheel
6666
run: uv build
6767
- name: Twine check

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,54 @@ print(to_json(ops, indent=4))
4343
# }
4444
# ]
4545
```
46+
47+
## Proxy-based patch generation
48+
49+
For better performance, `produce()` can be used which generates patches by tracking mutations on a proxy object (inspired by [Immer](https://immerjs.github.io/immer/produce)):
50+
51+
```python
52+
from patchdiff import produce
53+
54+
base = {"count": 0, "items": [1, 2, 3]}
55+
56+
def recipe(draft):
57+
"""Mutate the draft object - changes are tracked automatically."""
58+
draft["count"] = 5
59+
draft["items"].append(4)
60+
draft["new_field"] = "hello"
61+
62+
result, patches, reverse_patches = produce(base, recipe)
63+
64+
# base is unchanged (immutable by default)
65+
assert base == {"count": 0, "items": [1, 2, 3]}
66+
67+
# result contains the changes
68+
assert result == {"count": 5, "items": [1, 2, 3, 4], "new_field": "hello"}
69+
70+
# patches describe what changed
71+
print(patches)
72+
# [
73+
# {"op": "replace", "path": "/count", "value": 5},
74+
# {"op": "add", "path": "/items/-", "value": 4},
75+
# {"op": "add", "path": "/new_field", "value": "hello"}
76+
# ]
77+
```
78+
79+
When immutability is not needed, it is possible to apply the ops directly, improving performance even further by not having to make a `deepcopy` of the given state.
80+
81+
```python
82+
from observ import reactive
83+
from patchdiff import produce
84+
85+
state = reactive({"count": 0})
86+
87+
# Mutate in place and get patches for undo/redo
88+
result, patches, reverse = produce(
89+
state,
90+
lambda draft: draft.update({"count": 5}),
91+
in_place=True,
92+
)
93+
94+
assert result is state # Same object
95+
assert state["count"] == 5 # State was mutated, watchers triggered
96+
```

0 commit comments

Comments
 (0)