patchdiff 0.3.5__py3-none-any.whl → 0.3.7__py3-none-any.whl

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/__init__.py CHANGED
@@ -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"
patchdiff/apply.py CHANGED
@@ -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"]
patchdiff/diff.py CHANGED
@@ -1,4 +1,5 @@
1
- from functools import partial, reduce
1
+ from __future__ import annotations
2
+
2
3
  from typing import Dict, List, Set, Tuple
3
4
 
4
5
  from .pointer import Pointer
@@ -6,126 +7,178 @@ from .types import Diffable
6
7
 
7
8
 
8
9
  def diff_lists(input: List, output: List, ptr: Pointer) -> Tuple[List, List]:
9
- memory = {(0, 0): {"ops": [], "rops": [], "cost": 0}}
10
+ m, n = len(input), len(output)
11
+
12
+ # Build DP table bottom-up (iterative approach)
13
+ # dp[i][j] = cost of transforming input[0:i] to output[0:j]
14
+ dp = [[0] * (n + 1) for _ in range(m + 1)]
10
15
 
11
- def dist(i, j):
12
- if (i, j) not in memory:
13
- if i > 0 and j > 0 and input[i - 1] == output[j - 1]:
14
- step = dist(i - 1, j - 1)
16
+ # Initialize base cases
17
+ for i in range(1, m + 1):
18
+ dp[i][0] = i # Cost of deleting all elements
19
+ for j in range(1, n + 1):
20
+ dp[0][j] = j # Cost of adding all elements
21
+
22
+ # Fill DP table
23
+ for i in range(1, m + 1):
24
+ for j in range(1, n + 1):
25
+ if input[i - 1] == output[j - 1]:
26
+ # Elements match, no operation needed
27
+ dp[i][j] = dp[i - 1][j - 1]
15
28
  else:
16
- paths = []
17
- if i > 0:
18
- base = dist(i - 1, j)
19
- op = {"op": "remove", "idx": i - 1}
20
- rop = {"op": "add", "idx": j - 1, "value": input[i - 1]}
21
- paths.append(
22
- {
23
- "ops": base["ops"] + [op],
24
- "rops": base["rops"] + [rop],
25
- "cost": base["cost"] + 1,
26
- }
27
- )
28
- if j > 0:
29
- base = dist(i, j - 1)
30
- op = {"op": "add", "idx": i - 1, "value": output[j - 1]}
31
- rop = {"op": "remove", "idx": j - 1}
32
- paths.append(
33
- {
34
- "ops": base["ops"] + [op],
35
- "rops": base["rops"] + [rop],
36
- "cost": base["cost"] + 1,
37
- }
38
- )
39
- if i > 0 and j > 0:
40
- base = dist(i - 1, j - 1)
41
- op = {
42
- "op": "replace",
43
- "idx": i - 1,
44
- "original": input[i - 1],
45
- "value": output[j - 1],
46
- }
47
- rop = {
48
- "op": "replace",
49
- "idx": j - 1,
50
- "original": output[j - 1],
51
- "value": input[i - 1],
52
- }
53
- paths.append(
54
- {
55
- "ops": base["ops"] + [op],
56
- "rops": base["rops"] + [rop],
57
- "cost": base["cost"] + 1,
58
- }
59
- )
60
- step = min(paths, key=lambda a: a["cost"])
61
- memory[(i, j)] = step
62
- return memory[(i, j)]
63
-
64
- def pad(state, op, target=None):
65
- ops, padding = state
29
+ # Take minimum of three operations
30
+ dp[i][j] = min(
31
+ dp[i - 1][j] + 1, # Remove from input
32
+ dp[i][j - 1] + 1, # Add from output
33
+ dp[i - 1][j - 1] + 1, # Replace
34
+ )
35
+
36
+ # Traceback to extract operations
37
+ ops = []
38
+ rops = []
39
+ i, j = m, n
40
+
41
+ while i > 0 or j > 0:
42
+ if i > 0 and j > 0 and input[i - 1] == output[j - 1]:
43
+ # Elements match, no operation
44
+ i -= 1
45
+ j -= 1
46
+ elif i > 0 and (j == 0 or dp[i][j] == dp[i - 1][j] + 1):
47
+ # Remove from input
48
+ ops.append({"op": "remove", "idx": i - 1})
49
+ rops.append({"op": "add", "idx": j - 1, "value": input[i - 1]})
50
+ i -= 1
51
+ elif j > 0 and (i == 0 or dp[i][j] == dp[i][j - 1] + 1):
52
+ # Add from output
53
+ ops.append({"op": "add", "idx": i - 1, "value": output[j - 1]})
54
+ rops.append({"op": "remove", "idx": j - 1})
55
+ j -= 1
56
+ else:
57
+ # Replace
58
+ ops.append(
59
+ {
60
+ "op": "replace",
61
+ "idx": i - 1,
62
+ "original": input[i - 1],
63
+ "value": output[j - 1],
64
+ }
65
+ )
66
+ rops.append(
67
+ {
68
+ "op": "replace",
69
+ "idx": j - 1,
70
+ "original": output[j - 1],
71
+ "value": input[i - 1],
72
+ }
73
+ )
74
+ i -= 1
75
+ j -= 1
76
+
77
+ # Apply padding to operations (using explicit loops instead of reduce)
78
+ padded_ops = []
79
+ padding = 0
80
+ # Iterate in reverse to get correct order (traceback extracts operations backwards)
81
+ for op in reversed(ops):
66
82
  if op["op"] == "add":
67
83
  padded_idx = op["idx"] + 1 + padding
68
- idx_token = padded_idx if padded_idx < len(target) + padding else "-"
69
- full_op = {
70
- "op": "add",
71
- "path": ptr.append(idx_token),
72
- "value": op["value"],
73
- }
74
- return [[*ops, full_op], padding + 1]
84
+ idx_token = padded_idx if padded_idx < len(input) + padding else "-"
85
+ padded_ops.append(
86
+ {
87
+ "op": "add",
88
+ "path": ptr.append(idx_token),
89
+ "value": op["value"],
90
+ }
91
+ )
92
+ padding += 1
75
93
  elif op["op"] == "remove":
76
- full_op = {
77
- "op": "remove",
78
- "path": ptr.append(op["idx"] + padding),
79
- }
80
- return [[*ops, full_op], padding - 1]
81
- else:
94
+ padded_ops.append(
95
+ {
96
+ "op": "remove",
97
+ "path": ptr.append(op["idx"] + padding),
98
+ }
99
+ )
100
+ padding -= 1
101
+ else: # replace
82
102
  replace_ptr = ptr.append(op["idx"] + padding)
83
103
  replace_ops, _ = diff(op["original"], op["value"], replace_ptr)
84
- return [ops + replace_ops, padding]
104
+ padded_ops.extend(replace_ops)
85
105
 
86
- solution = dist(len(input), len(output))
87
- padded_ops, _ = reduce(partial(pad, target=input), solution["ops"], [[], 0])
88
- padded_rops, _ = reduce(partial(pad, target=output), solution["rops"], [[], 0])
106
+ padded_rops = []
107
+ padding = 0
108
+ # Iterate in reverse to get correct order (traceback extracts operations backwards)
109
+ for op in reversed(rops):
110
+ if op["op"] == "add":
111
+ padded_idx = op["idx"] + 1 + padding
112
+ idx_token = padded_idx if padded_idx < len(output) + padding else "-"
113
+ padded_rops.append(
114
+ {
115
+ "op": "add",
116
+ "path": ptr.append(idx_token),
117
+ "value": op["value"],
118
+ }
119
+ )
120
+ padding += 1
121
+ elif op["op"] == "remove":
122
+ padded_rops.append(
123
+ {
124
+ "op": "remove",
125
+ "path": ptr.append(op["idx"] + padding),
126
+ }
127
+ )
128
+ padding -= 1
129
+ else: # replace
130
+ replace_ptr = ptr.append(op["idx"] + padding)
131
+ replace_ops, _ = diff(op["original"], op["value"], replace_ptr)
132
+ padded_rops.extend(replace_ops)
89
133
 
90
134
  return padded_ops, padded_rops
91
135
 
92
136
 
93
137
  def diff_dicts(input: Dict, output: Dict, ptr: Pointer) -> Tuple[List, List]:
94
138
  ops, rops = [], []
95
- input_keys = set(input.keys())
96
- output_keys = set(output.keys())
97
- for key in input_keys - output_keys:
98
- ops.append({"op": "remove", "path": ptr.append(key)})
99
- rops.insert(0, {"op": "add", "path": ptr.append(key), "value": input[key]})
100
- for key in output_keys - input_keys:
101
- ops.append(
102
- {
103
- "op": "add",
104
- "path": ptr.append(key),
105
- "value": output[key],
106
- }
107
- )
108
- rops.insert(0, {"op": "remove", "path": ptr.append(key)})
109
- for key in input_keys & output_keys:
110
- key_ops, key_rops = diff(input[key], output[key], ptr.append(key))
111
- ops.extend(key_ops)
112
- key_rops.extend(rops)
113
- 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
114
163
  return ops, rops
115
164
 
116
165
 
117
166
  def diff_sets(input: Set, output: Set, ptr: Pointer) -> Tuple[List, List]:
118
167
  ops, rops = [], []
119
- for value in input - output:
120
- ops.append({"op": "remove", "path": ptr.append(value)})
121
- rops.insert(0, {"op": "add", "path": ptr.append("-"), "value": value})
122
- for value in output - input:
123
- ops.append({"op": "add", "path": ptr.append("-"), "value": value})
124
- 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)})
125
176
  return ops, rops
126
177
 
127
178
 
128
- def diff(input: Diffable, output: Diffable, ptr: Pointer = None) -> Tuple[List, List]:
179
+ def diff(
180
+ input: Diffable, output: Diffable, ptr: Pointer | None = None
181
+ ) -> Tuple[List, List]:
129
182
  if input == output:
130
183
  return [], []
131
184
  if ptr is None:
patchdiff/pointer.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- from typing import Any, Hashable, List, Tuple
4
+ from typing import Any, Hashable, Iterable, Tuple
5
5
 
6
6
  from .types import Diffable
7
7
 
@@ -20,7 +20,9 @@ def escape(token: str) -> str:
20
20
 
21
21
 
22
22
  class Pointer:
23
- def __init__(self, tokens: List[Hashable] | None = None) -> None:
23
+ __slots__ = ("tokens",)
24
+
25
+ def __init__(self, tokens: Iterable[Hashable] | None = None) -> None:
24
26
  if tokens is None:
25
27
  tokens = []
26
28
  self.tokens = tuple(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
  Metadata-Version: 2.4
2
2
  Name: patchdiff
3
- Version: 0.3.5
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>
@@ -0,0 +1,9 @@
1
+ patchdiff/__init__.py,sha256=tXjBK8Oei1NjMt-U4gRhshdJ9yYoRq5Y-xf0m3nHLq8,163
2
+ patchdiff/apply.py,sha256=zxjJ4sCivcy4WOZulQGfv2iduVeY0xnjlvZJkHIZhtY,1405
3
+ patchdiff/diff.py,sha256=-Aj-8NGE1HRoLxVYNK1dFofACaY3fCCsIJo6ypkrl5U,6827
4
+ patchdiff/pointer.py,sha256=MBakmqcm__2vrorFtMeJekeA8bWS-5mh384UG0u4E0w,1790
5
+ patchdiff/serialize.py,sha256=N0S9e0P49TBMb7ghM--h13MsF59ybiscjZ_auAErTq8,295
6
+ patchdiff/types.py,sha256=BVKXOl3tnQOOml3VI_epTrLn79agi6sD5vNr2acC-yE,77
7
+ patchdiff-0.3.7.dist-info/METADATA,sha256=d35C8963EBLsfTkqYNNriTO1aXYFKnJNEPh3tVYF0Es,1634
8
+ patchdiff-0.3.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ patchdiff-0.3.7.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- patchdiff/__init__.py,sha256=uQAwiWM9rTop0zZC6Spn0y_v74cfAzuhCxcEFOrAaXY,110
2
- patchdiff/apply.py,sha256=25TfOTZTYFIjy70mBus5yCzmbJRawbguDffv_4yHjqE,1366
3
- patchdiff/diff.py,sha256=AQuIhGDnZda-9IykepEHXcfrDJSEJPXuGe8SdLQoquU,5424
4
- patchdiff/pointer.py,sha256=ZZqJtGYM2uK6P2saOBn0LWxpIb_qgdTFlcE4EQncnRU,1804
5
- patchdiff/serialize.py,sha256=N0S9e0P49TBMb7ghM--h13MsF59ybiscjZ_auAErTq8,295
6
- patchdiff/types.py,sha256=BVKXOl3tnQOOml3VI_epTrLn79agi6sD5vNr2acC-yE,77
7
- patchdiff-0.3.5.dist-info/METADATA,sha256=pea0_lmfx8Y5g5RMNpQGfSJWfQiDQQHFv_yybEQUMkg,1634
8
- patchdiff-0.3.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- patchdiff-0.3.5.dist-info/RECORD,,