patchdiff 0.3.7__tar.gz → 0.3.9__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.
- {patchdiff-0.3.7 → patchdiff-0.3.9}/.github/workflows/benchmark.yml +6 -6
- patchdiff-0.3.9/.github/workflows/ci.yml +103 -0
- {patchdiff-0.3.7 → patchdiff-0.3.9}/PKG-INFO +53 -2
- patchdiff-0.3.9/README.md +96 -0
- patchdiff-0.3.9/benchmarks/benchmark.py +711 -0
- {patchdiff-0.3.7 → patchdiff-0.3.9}/patchdiff/__init__.py +1 -0
- {patchdiff-0.3.7 → patchdiff-0.3.9}/patchdiff/pointer.py +3 -3
- patchdiff-0.3.9/patchdiff/produce.py +776 -0
- {patchdiff-0.3.7 → patchdiff-0.3.9}/pyproject.toml +8 -2
- {patchdiff-0.3.7 → patchdiff-0.3.9}/tests/test_apply.py +23 -0
- patchdiff-0.3.9/tests/test_observ_integration.py +325 -0
- patchdiff-0.3.9/tests/test_produce_core.py +871 -0
- patchdiff-0.3.9/tests/test_produce_dict.py +706 -0
- patchdiff-0.3.9/tests/test_produce_list.py +1141 -0
- patchdiff-0.3.9/tests/test_produce_set.py +682 -0
- patchdiff-0.3.7/.github/workflows/ci.yml +0 -103
- patchdiff-0.3.7/README.md +0 -45
- patchdiff-0.3.7/benchmarks/benchmark.py +0 -232
- {patchdiff-0.3.7 → patchdiff-0.3.9}/.gitignore +0 -0
- {patchdiff-0.3.7 → patchdiff-0.3.9}/patchdiff/apply.py +0 -0
- {patchdiff-0.3.7 → patchdiff-0.3.9}/patchdiff/diff.py +0 -0
- {patchdiff-0.3.7 → patchdiff-0.3.9}/patchdiff/serialize.py +0 -0
- {patchdiff-0.3.7 → patchdiff-0.3.9}/patchdiff/types.py +0 -0
- {patchdiff-0.3.7 → patchdiff-0.3.9}/tests/test_diff.py +0 -0
- {patchdiff-0.3.7 → patchdiff-0.3.9}/tests/test_pointer.py +0 -0
- {patchdiff-0.3.7 → patchdiff-0.3.9}/tests/test_proxy.py +0 -0
- {patchdiff-0.3.7 → patchdiff-0.3.9}/tests/test_serialize.py +0 -0
|
@@ -10,18 +10,18 @@ jobs:
|
|
|
10
10
|
name: Benchmarks
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
14
|
|
|
15
15
|
- name: Install uv
|
|
16
|
-
uses: astral-sh/setup-uv@
|
|
16
|
+
uses: astral-sh/setup-uv@v7
|
|
17
17
|
|
|
18
18
|
- name: Set up Python 3.14
|
|
19
|
-
uses: actions/setup-python@
|
|
19
|
+
uses: actions/setup-python@v6
|
|
20
20
|
with:
|
|
21
21
|
python-version: "3.14"
|
|
22
22
|
|
|
23
23
|
- name: Install dependencies
|
|
24
|
-
run: uv sync
|
|
24
|
+
run: uv sync --group observ
|
|
25
25
|
|
|
26
26
|
# On PRs: run benchmarks twice (PR code vs master code) and compare
|
|
27
27
|
- name: Run benchmarks
|
|
@@ -49,8 +49,8 @@ jobs:
|
|
|
49
49
|
|
|
50
50
|
- name: Upload benchmarks
|
|
51
51
|
if: always()
|
|
52
|
-
uses: actions/upload-artifact@
|
|
52
|
+
uses: actions/upload-artifact@v6
|
|
53
53
|
with:
|
|
54
54
|
name: Benchmarks
|
|
55
55
|
path: .benchmarks/
|
|
56
|
-
include-hidden-files: true
|
|
56
|
+
include-hidden-files: true
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
tags:
|
|
8
|
+
- "v*"
|
|
9
|
+
pull_request:
|
|
10
|
+
branches:
|
|
11
|
+
- master
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
name: Lint and test on ${{ matrix.name }}
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
include:
|
|
21
|
+
- name: Linux py38
|
|
22
|
+
pyversion: "3.8"
|
|
23
|
+
- name: Linux py39
|
|
24
|
+
pyversion: "3.9"
|
|
25
|
+
- name: Linux py310
|
|
26
|
+
pyversion: "3.10"
|
|
27
|
+
- name: Linux py311
|
|
28
|
+
pyversion: "3.11"
|
|
29
|
+
- name: Linux py312
|
|
30
|
+
pyversion: "3.12"
|
|
31
|
+
- name: Linux py313
|
|
32
|
+
pyversion: "3.13"
|
|
33
|
+
- name: Linux py314
|
|
34
|
+
pyversion: "3.14"
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v6
|
|
37
|
+
- name: Install uv
|
|
38
|
+
uses: astral-sh/setup-uv@v7
|
|
39
|
+
- name: Set up Python ${{ matrix.pyversion }}
|
|
40
|
+
uses: actions/setup-python@v6
|
|
41
|
+
with:
|
|
42
|
+
python-version: ${{ matrix.pyversion }}
|
|
43
|
+
- name: Install dependencies
|
|
44
|
+
run: uv sync --group observ
|
|
45
|
+
- name: Lint
|
|
46
|
+
run: uv run ruff check
|
|
47
|
+
- name: Format
|
|
48
|
+
run: uv run ruff format --check
|
|
49
|
+
- name: Test
|
|
50
|
+
run: uv run pytest -v --cov=patchdiff --cov-report=term-missing --cov-fail-under=100
|
|
51
|
+
|
|
52
|
+
build:
|
|
53
|
+
name: Build and test wheel
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v6
|
|
57
|
+
- name: Install uv
|
|
58
|
+
uses: astral-sh/setup-uv@v7
|
|
59
|
+
- name: Set up Python 3.9
|
|
60
|
+
uses: actions/setup-python@v6
|
|
61
|
+
with:
|
|
62
|
+
python-version: "3.9"
|
|
63
|
+
- name: Install dependencies
|
|
64
|
+
run: uv sync --group observ
|
|
65
|
+
- name: Build wheel
|
|
66
|
+
run: uv build
|
|
67
|
+
- name: Twine check
|
|
68
|
+
run: uvx twine check dist/*
|
|
69
|
+
- name: Upload wheel artifact
|
|
70
|
+
uses: actions/upload-artifact@v6
|
|
71
|
+
with:
|
|
72
|
+
path: dist
|
|
73
|
+
name: dist
|
|
74
|
+
|
|
75
|
+
publish:
|
|
76
|
+
name: Publish to Github and Pypi
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
needs: [test, build]
|
|
79
|
+
if: success() && startsWith(github.ref, 'refs/tags/v')
|
|
80
|
+
environment:
|
|
81
|
+
name: pypi
|
|
82
|
+
url: https://pypi.org/p/patchdiff
|
|
83
|
+
permissions:
|
|
84
|
+
id-token: write
|
|
85
|
+
contents: write
|
|
86
|
+
steps:
|
|
87
|
+
- uses: actions/checkout@v6
|
|
88
|
+
- name: Download wheel artifact
|
|
89
|
+
uses: actions/download-artifact@v8
|
|
90
|
+
with:
|
|
91
|
+
name: dist
|
|
92
|
+
path: dist
|
|
93
|
+
- name: Release
|
|
94
|
+
uses: softprops/action-gh-release@v2
|
|
95
|
+
with:
|
|
96
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
97
|
+
files: |
|
|
98
|
+
dist/*.tar.gz
|
|
99
|
+
dist/*.whl
|
|
100
|
+
draft: true
|
|
101
|
+
prerelease: false
|
|
102
|
+
- name: Publish to PyPI
|
|
103
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchdiff
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: MIT
|
|
5
5
|
Project-URL: Homepage, https://github.com/fork-tongue/patchdiff
|
|
6
6
|
Author-email: Korijn van Golen <korijn@gmail.com>, Berend Klein Haneveld <berendkleinhaneveld@gmail.com>
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
|
|
10
10
|
[](https://badge.fury.io/py/patchdiff)
|
|
@@ -52,3 +52,54 @@ print(to_json(ops, indent=4))
|
|
|
52
52
|
# }
|
|
53
53
|
# ]
|
|
54
54
|
```
|
|
55
|
+
|
|
56
|
+
## Proxy-based patch generation
|
|
57
|
+
|
|
58
|
+
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)):
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from patchdiff import produce
|
|
62
|
+
|
|
63
|
+
base = {"count": 0, "items": [1, 2, 3]}
|
|
64
|
+
|
|
65
|
+
def recipe(draft):
|
|
66
|
+
"""Mutate the draft object - changes are tracked automatically."""
|
|
67
|
+
draft["count"] = 5
|
|
68
|
+
draft["items"].append(4)
|
|
69
|
+
draft["new_field"] = "hello"
|
|
70
|
+
|
|
71
|
+
result, patches, reverse_patches = produce(base, recipe)
|
|
72
|
+
|
|
73
|
+
# base is unchanged (immutable by default)
|
|
74
|
+
assert base == {"count": 0, "items": [1, 2, 3]}
|
|
75
|
+
|
|
76
|
+
# result contains the changes
|
|
77
|
+
assert result == {"count": 5, "items": [1, 2, 3, 4], "new_field": "hello"}
|
|
78
|
+
|
|
79
|
+
# patches describe what changed
|
|
80
|
+
print(patches)
|
|
81
|
+
# [
|
|
82
|
+
# {"op": "replace", "path": "/count", "value": 5},
|
|
83
|
+
# {"op": "add", "path": "/items/-", "value": 4},
|
|
84
|
+
# {"op": "add", "path": "/new_field", "value": "hello"}
|
|
85
|
+
# ]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from observ import reactive
|
|
92
|
+
from patchdiff import produce
|
|
93
|
+
|
|
94
|
+
state = reactive({"count": 0})
|
|
95
|
+
|
|
96
|
+
# Mutate in place and get patches for undo/redo
|
|
97
|
+
result, patches, reverse = produce(
|
|
98
|
+
state,
|
|
99
|
+
lambda draft: draft.update({"count": 5}),
|
|
100
|
+
in_place=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
assert result is state # Same object
|
|
104
|
+
assert state["count"] == 5 # State was mutated, watchers triggered
|
|
105
|
+
```
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
[](https://badge.fury.io/py/patchdiff)
|
|
2
|
+
[](https://github.com/fork-tongue/patchdiff/actions)
|
|
3
|
+
|
|
4
|
+
# Patchdiff 🔍
|
|
5
|
+
|
|
6
|
+
Based on [rfc6902](https://github.com/chbrown/rfc6902) this library provides a simple API to generate bi-directional diffs between composite python datastructures composed out of lists, sets, tuples and dicts. The diffs are jsonpatch compliant, and can optionally be serialized to json format. Patchdiff can also be used to apply lists of patches to objects, both in-place or on a deepcopy of the input.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
`pip install patchdiff`
|
|
11
|
+
|
|
12
|
+
## Quick-start
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from patchdiff import apply, diff, iapply, to_json
|
|
16
|
+
|
|
17
|
+
input = {"a": [5, 7, 9, {"a", "b", "c"}], "b": 6}
|
|
18
|
+
output = {"a": [5, 2, 9, {"b", "c"}], "b": 6, "c": 7}
|
|
19
|
+
|
|
20
|
+
ops, reverse_ops = diff(input, output)
|
|
21
|
+
|
|
22
|
+
assert apply(input, ops) == output
|
|
23
|
+
assert apply(output, reverse_ops) == input
|
|
24
|
+
|
|
25
|
+
iapply(input, ops) # apply in-place
|
|
26
|
+
assert input == output
|
|
27
|
+
|
|
28
|
+
print(to_json(ops, indent=4))
|
|
29
|
+
# [
|
|
30
|
+
# {
|
|
31
|
+
# "op": "add",
|
|
32
|
+
# "path": "/c",
|
|
33
|
+
# "value": 7
|
|
34
|
+
# },
|
|
35
|
+
# {
|
|
36
|
+
# "op": "replace",
|
|
37
|
+
# "path": "/a/1",
|
|
38
|
+
# "value": 2
|
|
39
|
+
# },
|
|
40
|
+
# {
|
|
41
|
+
# "op": "remove",
|
|
42
|
+
# "path": "/a/3/a"
|
|
43
|
+
# }
|
|
44
|
+
# ]
|
|
45
|
+
```
|
|
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
|
+
```
|