patchdiff 0.3.7__tar.gz → 0.3.8__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.
Files changed (27) hide show
  1. {patchdiff-0.3.7 → patchdiff-0.3.8}/.github/workflows/benchmark.yml +6 -6
  2. patchdiff-0.3.8/.github/workflows/ci.yml +103 -0
  3. {patchdiff-0.3.7 → patchdiff-0.3.8}/PKG-INFO +53 -2
  4. patchdiff-0.3.8/README.md +96 -0
  5. patchdiff-0.3.8/benchmarks/benchmark.py +711 -0
  6. {patchdiff-0.3.7 → patchdiff-0.3.8}/patchdiff/__init__.py +1 -0
  7. {patchdiff-0.3.7 → patchdiff-0.3.8}/patchdiff/pointer.py +3 -3
  8. patchdiff-0.3.8/patchdiff/produce.py +660 -0
  9. {patchdiff-0.3.7 → patchdiff-0.3.8}/pyproject.toml +8 -2
  10. patchdiff-0.3.8/tests/test_observ_integration.py +325 -0
  11. patchdiff-0.3.8/tests/test_produce_core.py +795 -0
  12. patchdiff-0.3.8/tests/test_produce_dict.py +580 -0
  13. patchdiff-0.3.8/tests/test_produce_list.py +984 -0
  14. patchdiff-0.3.8/tests/test_produce_set.py +537 -0
  15. patchdiff-0.3.7/.github/workflows/ci.yml +0 -103
  16. patchdiff-0.3.7/README.md +0 -45
  17. patchdiff-0.3.7/benchmarks/benchmark.py +0 -232
  18. {patchdiff-0.3.7 → patchdiff-0.3.8}/.gitignore +0 -0
  19. {patchdiff-0.3.7 → patchdiff-0.3.8}/patchdiff/apply.py +0 -0
  20. {patchdiff-0.3.7 → patchdiff-0.3.8}/patchdiff/diff.py +0 -0
  21. {patchdiff-0.3.7 → patchdiff-0.3.8}/patchdiff/serialize.py +0 -0
  22. {patchdiff-0.3.7 → patchdiff-0.3.8}/patchdiff/types.py +0 -0
  23. {patchdiff-0.3.7 → patchdiff-0.3.8}/tests/test_apply.py +0 -0
  24. {patchdiff-0.3.7 → patchdiff-0.3.8}/tests/test_diff.py +0 -0
  25. {patchdiff-0.3.7 → patchdiff-0.3.8}/tests/test_pointer.py +0 -0
  26. {patchdiff-0.3.7 → patchdiff-0.3.8}/tests/test_proxy.py +0 -0
  27. {patchdiff-0.3.7 → patchdiff-0.3.8}/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@v5
13
+ - uses: actions/checkout@v6
14
14
 
15
15
  - name: Install uv
16
- uses: astral-sh/setup-uv@v6
16
+ uses: astral-sh/setup-uv@v7
17
17
 
18
18
  - name: Set up Python 3.14
19
- uses: actions/setup-python@v5
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@v4
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
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.7
3
+ Version: 0.3.8
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.8
7
+ Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
9
 
10
10
  [![PyPI version](https://badge.fury.io/py/patchdiff.svg)](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
+ [![PyPI version](https://badge.fury.io/py/patchdiff.svg)](https://badge.fury.io/py/patchdiff)
2
+ [![CI status](https://github.com/fork-tongue/patchdiff/workflows/CI/badge.svg)](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
+ ```