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.
@@ -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
@@ -5,3 +5,4 @@ __pycache__
5
5
  dist
6
6
  uv.lock
7
7
  .benchmarks/
8
+ .venv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchdiff
3
- Version: 0.3.6
3
+ Version: 0.3.7
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>
@@ -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")
@@ -1,5 +1,7 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("patchdiff")
4
+
1
5
  from .apply import apply, iapply
2
6
  from .diff import diff
3
7
  from .serialize import to_json
4
-
5
- __version__ = "0.3.4"
@@ -6,6 +6,8 @@ from .types import Diffable
6
6
 
7
7
  def iapply(obj: Diffable, patches: List[Dict]) -> Diffable:
8
8
  """Apply a set of patches to an object, in-place"""
9
+ if not patches:
10
+ return obj
9
11
  for patch in patches:
10
12
  ptr = patch["path"]
11
13
  op = patch["op"]
@@ -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
- for key in input_keys - output_keys:
142
- ops.append({"op": "remove", "path": ptr.append(key)})
143
- rops.insert(0, {"op": "add", "path": ptr.append(key), "value": input[key]})
144
- for key in output_keys - input_keys:
145
- ops.append(
146
- {
147
- "op": "add",
148
- "path": ptr.append(key),
149
- "value": output[key],
150
- }
151
- )
152
- rops.insert(0, {"op": "remove", "path": ptr.append(key)})
153
- for key in input_keys & output_keys:
154
- key_ops, key_rops = diff(input[key], output[key], ptr.append(key))
155
- ops.extend(key_ops)
156
- key_rops.extend(rops)
157
- rops = key_rops
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
- for value in input - output:
164
- ops.append({"op": "remove", "path": ptr.append(value)})
165
- rops.insert(0, {"op": "add", "path": ptr.append("-"), "value": value})
166
- for value in output - input:
167
- ops.append({"op": "add", "path": ptr.append("-"), "value": value})
168
- rops.insert(0, {"op": "remove", "path": ptr.append(value)})
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 not isinstance(other, self.__class__):
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
- for key in self.tokens:
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
- cursor = parent[key]
60
- except KeyError:
61
- break
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "patchdiff"
3
- version = "0.3.6"
3
+ version = "0.3.7"
4
4
  description = "MIT"
5
5
  authors = [
6
6
  { name = "Korijn van Golen", email = "korijn@gmail.com" },
@@ -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