patchdiff 0.3.6__tar.gz → 0.3.7__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/.github/workflows/benchmark.yml +56 -0
- {patchdiff-0.3.6 → patchdiff-0.3.7}/.gitignore +1 -0
- {patchdiff-0.3.6 → patchdiff-0.3.7}/PKG-INFO +1 -1
- {patchdiff-0.3.6 → patchdiff-0.3.7}/benchmarks/benchmark.py +66 -0
- {patchdiff-0.3.6 → patchdiff-0.3.7}/patchdiff/__init__.py +4 -2
- {patchdiff-0.3.6 → patchdiff-0.3.7}/patchdiff/apply.py +2 -0
- {patchdiff-0.3.6 → patchdiff-0.3.7}/patchdiff/diff.py +32 -25
- {patchdiff-0.3.6 → patchdiff-0.3.7}/patchdiff/pointer.py +10 -11
- {patchdiff-0.3.6 → patchdiff-0.3.7}/pyproject.toml +1 -1
- patchdiff-0.3.6/.github/workflows/benchmark.yml +0 -69
- {patchdiff-0.3.6 → patchdiff-0.3.7}/.github/workflows/ci.yml +0 -0
- {patchdiff-0.3.6 → patchdiff-0.3.7}/README.md +0 -0
- {patchdiff-0.3.6 → patchdiff-0.3.7}/patchdiff/serialize.py +0 -0
- {patchdiff-0.3.6 → patchdiff-0.3.7}/patchdiff/types.py +0 -0
- {patchdiff-0.3.6 → patchdiff-0.3.7}/tests/test_apply.py +0 -0
- {patchdiff-0.3.6 → patchdiff-0.3.7}/tests/test_diff.py +0 -0
- {patchdiff-0.3.6 → patchdiff-0.3.7}/tests/test_pointer.py +0 -0
- {patchdiff-0.3.6 → patchdiff-0.3.7}/tests/test_proxy.py +0 -0
- {patchdiff-0.3.6 → patchdiff-0.3.7}/tests/test_serialize.py +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: Benchmarks
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
benchmark:
|
|
10
|
+
name: Benchmarks
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v5
|
|
14
|
+
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v6
|
|
17
|
+
|
|
18
|
+
- name: Set up Python 3.14
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.14"
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: uv sync
|
|
25
|
+
|
|
26
|
+
# On PRs: run benchmarks twice (PR code vs master code) and compare
|
|
27
|
+
- name: Run benchmarks
|
|
28
|
+
run: |
|
|
29
|
+
# Checkout master version of patchdiff directory
|
|
30
|
+
git fetch origin master
|
|
31
|
+
git checkout origin/master -- patchdiff/
|
|
32
|
+
|
|
33
|
+
# Run benchmarks with master code as baseline
|
|
34
|
+
uv run --no-sync pytest benchmarks/benchmark.py \
|
|
35
|
+
--benchmark-only \
|
|
36
|
+
--benchmark-save=master \
|
|
37
|
+
--benchmark-sort=mean || true
|
|
38
|
+
|
|
39
|
+
# Restore PR code
|
|
40
|
+
git checkout HEAD -- patchdiff/
|
|
41
|
+
|
|
42
|
+
# Run benchmarks on PR code and compare
|
|
43
|
+
uv run --no-sync pytest benchmarks/benchmark.py \
|
|
44
|
+
--benchmark-only \
|
|
45
|
+
--benchmark-compare \
|
|
46
|
+
--benchmark-compare-fail=mean:5% \
|
|
47
|
+
--benchmark-save=branch \
|
|
48
|
+
--benchmark-sort=mean
|
|
49
|
+
|
|
50
|
+
- name: Upload benchmarks
|
|
51
|
+
if: always()
|
|
52
|
+
uses: actions/upload-artifact@v4
|
|
53
|
+
with:
|
|
54
|
+
name: Benchmarks
|
|
55
|
+
path: .benchmarks/
|
|
56
|
+
include-hidden-files: true
|
|
@@ -19,6 +19,7 @@ import random
|
|
|
19
19
|
import pytest
|
|
20
20
|
|
|
21
21
|
from patchdiff import apply, diff
|
|
22
|
+
from patchdiff.pointer import Pointer
|
|
22
23
|
|
|
23
24
|
# Set seed for reproducibility
|
|
24
25
|
random.seed(42)
|
|
@@ -139,6 +140,27 @@ def test_dict_diff_nested(benchmark):
|
|
|
139
140
|
benchmark(diff, a, b)
|
|
140
141
|
|
|
141
142
|
|
|
143
|
+
# ========================================
|
|
144
|
+
# Set Diff Benchmarks
|
|
145
|
+
# ========================================
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@pytest.mark.benchmark(group="set-diff")
|
|
149
|
+
def test_set_diff_1000_elements(benchmark):
|
|
150
|
+
"""Benchmark: Sets with 1000 elements, 10% difference."""
|
|
151
|
+
a = set(generate_random_list(1000, 2000))
|
|
152
|
+
b = a.copy()
|
|
153
|
+
# Remove 5%
|
|
154
|
+
a_list = list(a)
|
|
155
|
+
for i in range(50):
|
|
156
|
+
a.remove(a_list[i])
|
|
157
|
+
# Add 5%
|
|
158
|
+
for i in range(50):
|
|
159
|
+
b.add(2000 + i)
|
|
160
|
+
|
|
161
|
+
benchmark(diff, a, b)
|
|
162
|
+
|
|
163
|
+
|
|
142
164
|
# ========================================
|
|
143
165
|
# Mixed Structure Benchmarks
|
|
144
166
|
# ========================================
|
|
@@ -164,3 +186,47 @@ def test_apply_list_1000_elements(benchmark):
|
|
|
164
186
|
ops, _ = diff(a, b)
|
|
165
187
|
|
|
166
188
|
benchmark(apply, a, ops)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ========================================
|
|
192
|
+
# Pointer Evaluate Benchmarks
|
|
193
|
+
# ========================================
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@pytest.mark.benchmark(group="pointer-evaluate")
|
|
197
|
+
def test_pointer_evaluate_deep_dict(benchmark):
|
|
198
|
+
"""Benchmark: Evaluate pointer on deeply nested structure."""
|
|
199
|
+
depth = 100
|
|
200
|
+
obj = 42
|
|
201
|
+
for i in range(depth - 1, -1, -1):
|
|
202
|
+
obj = {f"key_{i}": obj}
|
|
203
|
+
ptr = Pointer([f"key_{i}" for i in range(depth)])
|
|
204
|
+
|
|
205
|
+
benchmark(ptr.evaluate, obj)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@pytest.mark.benchmark(group="pointer-evaluate")
|
|
209
|
+
def test_pointer_evaluate_deep_list(benchmark):
|
|
210
|
+
"""Benchmark: Evaluate pointer on deep lists."""
|
|
211
|
+
# Build nested lists 100 levels deep; innermost value is 42.
|
|
212
|
+
depth = 100
|
|
213
|
+
nested = 42
|
|
214
|
+
for _ in range(depth):
|
|
215
|
+
nested = [nested]
|
|
216
|
+
obj = nested
|
|
217
|
+
ptr = Pointer([0] * depth)
|
|
218
|
+
|
|
219
|
+
benchmark(ptr.evaluate, obj)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ========================================
|
|
223
|
+
# Pointer Append Benchmarks
|
|
224
|
+
# ========================================
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@pytest.mark.benchmark(group="pointer-append")
|
|
228
|
+
def test_pointer_append(benchmark):
|
|
229
|
+
"""Benchmark: Append token to pointer."""
|
|
230
|
+
ptr = Pointer.from_str("/a/b/c/d/e/f/g/h/i/j")
|
|
231
|
+
|
|
232
|
+
benchmark(ptr.append, "k")
|
|
@@ -136,36 +136,43 @@ def diff_lists(input: List, output: List, ptr: Pointer) -> Tuple[List, List]:
|
|
|
136
136
|
|
|
137
137
|
def diff_dicts(input: Dict, output: Dict, ptr: Pointer) -> Tuple[List, List]:
|
|
138
138
|
ops, rops = [], []
|
|
139
|
-
input_keys = set(input.keys())
|
|
140
|
-
output_keys = set(output.keys())
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
139
|
+
input_keys = set(input.keys()) if input else set()
|
|
140
|
+
output_keys = set(output.keys()) if output else set()
|
|
141
|
+
if input_only := input_keys - output_keys:
|
|
142
|
+
for key in input_only:
|
|
143
|
+
key_ptr = ptr.append(key)
|
|
144
|
+
ops.append({"op": "remove", "path": key_ptr})
|
|
145
|
+
rops.insert(0, {"op": "add", "path": key_ptr, "value": input[key]})
|
|
146
|
+
if output_only := output_keys - input_keys:
|
|
147
|
+
for key in output_only:
|
|
148
|
+
key_ptr = ptr.append(key)
|
|
149
|
+
ops.append(
|
|
150
|
+
{
|
|
151
|
+
"op": "add",
|
|
152
|
+
"path": key_ptr,
|
|
153
|
+
"value": output[key],
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
rops.insert(0, {"op": "remove", "path": key_ptr})
|
|
157
|
+
if common := input_keys & output_keys:
|
|
158
|
+
for key in common:
|
|
159
|
+
key_ops, key_rops = diff(input[key], output[key], ptr.append(key))
|
|
160
|
+
ops.extend(key_ops)
|
|
161
|
+
key_rops.extend(rops)
|
|
162
|
+
rops = key_rops
|
|
158
163
|
return ops, rops
|
|
159
164
|
|
|
160
165
|
|
|
161
166
|
def diff_sets(input: Set, output: Set, ptr: Pointer) -> Tuple[List, List]:
|
|
162
167
|
ops, rops = [], []
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
if input_only := input - output:
|
|
169
|
+
for value in input_only:
|
|
170
|
+
ops.append({"op": "remove", "path": ptr.append(value)})
|
|
171
|
+
rops.insert(0, {"op": "add", "path": ptr.append("-"), "value": value})
|
|
172
|
+
if output_only := output - input:
|
|
173
|
+
for value in output_only:
|
|
174
|
+
ops.append({"op": "add", "path": ptr.append("-"), "value": value})
|
|
175
|
+
rops.insert(0, {"op": "remove", "path": ptr.append(value)})
|
|
169
176
|
return ops, rops
|
|
170
177
|
|
|
171
178
|
|
|
@@ -20,6 +20,8 @@ def escape(token: str) -> str:
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class Pointer:
|
|
23
|
+
__slots__ = ("tokens",)
|
|
24
|
+
|
|
23
25
|
def __init__(self, tokens: Iterable[Hashable] | None = None) -> None:
|
|
24
26
|
if tokens is None:
|
|
25
27
|
tokens = []
|
|
@@ -40,7 +42,7 @@ class Pointer:
|
|
|
40
42
|
return hash(self.tokens)
|
|
41
43
|
|
|
42
44
|
def __eq__(self, other: "Pointer") -> bool:
|
|
43
|
-
if
|
|
45
|
+
if other.__class__ != self.__class__:
|
|
44
46
|
return False
|
|
45
47
|
return self.tokens == other.tokens
|
|
46
48
|
|
|
@@ -48,17 +50,14 @@ class Pointer:
|
|
|
48
50
|
key = ""
|
|
49
51
|
parent = None
|
|
50
52
|
cursor = obj
|
|
51
|
-
|
|
52
|
-
parent = cursor
|
|
53
|
-
if hasattr(parent, "add"): # set
|
|
54
|
-
break
|
|
55
|
-
if hasattr(parent, "append"): # list
|
|
56
|
-
if key == "-":
|
|
57
|
-
break
|
|
53
|
+
if tokens := self.tokens:
|
|
58
54
|
try:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
for key in tokens:
|
|
56
|
+
parent = cursor
|
|
57
|
+
cursor = parent[key]
|
|
58
|
+
except (KeyError, TypeError):
|
|
59
|
+
# KeyError for dicts, TypeError for sets and lists
|
|
60
|
+
pass
|
|
62
61
|
return parent, key, cursor
|
|
63
62
|
|
|
64
63
|
def append(self, token: Hashable) -> "Pointer":
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
name: Benchmarks
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- master
|
|
7
|
-
pull_request:
|
|
8
|
-
branches:
|
|
9
|
-
- master
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
benchmark:
|
|
13
|
-
name: Benchmarks
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v5
|
|
17
|
-
|
|
18
|
-
- name: Install uv
|
|
19
|
-
uses: astral-sh/setup-uv@v6
|
|
20
|
-
|
|
21
|
-
- name: Set up Python 3.14
|
|
22
|
-
uses: actions/setup-python@v5
|
|
23
|
-
with:
|
|
24
|
-
python-version: '3.14'
|
|
25
|
-
|
|
26
|
-
- name: Install dependencies
|
|
27
|
-
run: uv sync
|
|
28
|
-
|
|
29
|
-
# Restore benchmark baseline (read-only for PRs)
|
|
30
|
-
- name: Restore benchmark baseline
|
|
31
|
-
uses: actions/cache/restore@v4
|
|
32
|
-
with:
|
|
33
|
-
path: .benchmarks
|
|
34
|
-
key: benchmark-baseline-3.14-${{ runner.os }}
|
|
35
|
-
|
|
36
|
-
# On master: save baseline results
|
|
37
|
-
- name: Run benchmarks and save baseline
|
|
38
|
-
if: github.ref == 'refs/heads/master'
|
|
39
|
-
continue-on-error: true
|
|
40
|
-
run: |
|
|
41
|
-
uv run --no-sync pytest benchmarks/benchmark.py \
|
|
42
|
-
--benchmark-only \
|
|
43
|
-
--benchmark-autosave \
|
|
44
|
-
--benchmark-sort=name
|
|
45
|
-
|
|
46
|
-
# On master: cache the new baseline results
|
|
47
|
-
- name: Save benchmark baseline
|
|
48
|
-
if: github.ref == 'refs/heads/master'
|
|
49
|
-
uses: actions/cache/save@v4
|
|
50
|
-
with:
|
|
51
|
-
path: .benchmarks
|
|
52
|
-
key: benchmark-baseline-3.14-${{ runner.os }}
|
|
53
|
-
|
|
54
|
-
# On PRs: compare against baseline and fail if degraded
|
|
55
|
-
- name: Run benchmarks and compare
|
|
56
|
-
if: github.event_name == 'pull_request'
|
|
57
|
-
run: |
|
|
58
|
-
if [ -z "$(uv run --no-sync pytest-benchmark list)" ]; then
|
|
59
|
-
echo "No baseline found, not comparing"
|
|
60
|
-
uv run --no-sync pytest -v benchmarks/benchmark.py
|
|
61
|
-
exit
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
uv run --no-sync pytest benchmarks/benchmark.py \
|
|
65
|
-
--benchmark-only \
|
|
66
|
-
--benchmark-compare \
|
|
67
|
-
--benchmark-compare-fail=mean:5% \
|
|
68
|
-
--benchmark-sort=name
|
|
69
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|