Skip to content

Commit 7aad4b7

Browse files
committed
tests pass, reverse ops implemented
1 parent edc679b commit 7aad4b7

File tree

7 files changed

+102
-49
lines changed

7 files changed

+102
-49
lines changed

patchdiff/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .apply import apply
1+
from .apply import apply, iapply
22
from .diff import diff
33

44
__version__ = "0.2.0"

patchdiff/apply.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from .types import Diffable
55

66

7-
def apply(obj: Diffable, patches: List[Dict]) -> Diffable:
7+
def iapply(obj: Diffable, patches: List[Dict]) -> Diffable:
8+
"""Apply a set of patches to an object, in-place"""
89
for patch in patches:
910
ptr = patch["path"]
1011
op = patch["op"]
@@ -34,3 +35,8 @@ def apply(obj: Diffable, patches: List[Dict]) -> Diffable:
3435
else: # add/replace
3536
parent[key] = value
3637
return obj
38+
39+
40+
def apply(obj: Diffable, patches: List[Dict]) -> Diffable:
41+
"""Apply a set of patches to a deep copy of an object"""
42+
return iapply(deepcopy(obj), patches)

patchdiff/diff.py

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from functools import reduce
2-
from typing import Dict, List, Set
2+
from typing import Dict, List, Set, Tuple
33

44
from .pointer import Pointer
55
from .types import Diffable
66

77

8-
def diff_lists(input: List, output: List, ptr: Pointer) -> List:
9-
memory = {(0, 0): {"ops": [], "cost": 0}}
8+
def diff_lists(input: List, output: List, ptr: Pointer) -> Tuple[List, List]:
9+
memory = {(0, 0): {"ops": [], "rops": [], "cost": 0}}
1010

1111
def dist(i, j):
1212
if (i, j) not in memory:
@@ -17,18 +17,22 @@ def dist(i, j):
1717
if i > 0:
1818
base = dist(i - 1, j)
1919
op = {"op": "remove", "idx": i - 1}
20+
rop = {"op": "add", "idx": i - 1, "value": input[i - 1]}
2021
paths.append(
2122
{
2223
"ops": base["ops"] + [op],
24+
"rops": base["rops"] + [rop],
2325
"cost": base["cost"] + 1,
2426
}
2527
)
2628
if j > 0:
2729
base = dist(i, j - 1)
2830
op = {"op": "add", "idx": j - 1, "value": output[j - 1]}
31+
rop = {"op": "remove", "idx": j - 1}
2932
paths.append(
3033
{
3134
"ops": base["ops"] + [op],
35+
"rops": base["rops"] + [rop],
3236
"cost": base["cost"] + 1,
3337
}
3438
)
@@ -40,18 +44,23 @@ def dist(i, j):
4044
"original": input[i - 1],
4145
"value": output[j - 1],
4246
}
47+
rop = {
48+
"op": "replace",
49+
"idx": i - 1,
50+
"original": output[j - 1],
51+
"value": input[i - 1],
52+
}
4353
paths.append(
4454
{
4555
"ops": base["ops"] + [op],
56+
"rops": base["rops"] + [rop],
4657
"cost": base["cost"] + 1,
4758
}
4859
)
4960
step = min(paths, key=lambda a: a["cost"])
5061
memory[(i, j)] = step
5162
return memory[(i, j)]
5263

53-
ops = dist(len(input), len(output))["ops"]
54-
5564
def pad(state, op):
5665
ops, padding = state
5766
if op["op"] == "add":
@@ -71,46 +80,54 @@ def pad(state, op):
7180
return [ops + [full_op], padding - 1]
7281
else:
7382
replace_ptr = ptr.append(op["idx"] + padding)
74-
replace_ops = diff(op["original"], op["value"], replace_ptr)
83+
replace_ops, _ = diff(op["original"], op["value"], replace_ptr)
7584
return [ops + replace_ops, padding]
7685

77-
padded_ops, _ = reduce(pad, ops, [[], 0])
86+
solution = dist(len(input), len(output))
87+
padded_ops, _ = reduce(pad, solution["ops"], [[], 0])
88+
padded_rops, _ = reduce(pad, reversed(solution["rops"]), [[], 0])
7889

79-
return padded_ops
90+
return padded_ops, padded_rops
8091

8192

82-
def diff_dicts(input: Dict, output: Dict, ptr: Pointer) -> List:
83-
ops = []
93+
def diff_dicts(input: Dict, output: Dict, ptr: Pointer) -> Tuple[List, List]:
94+
ops, rops = [], []
8495
input_keys = set(input.keys())
8596
output_keys = set(output.keys())
8697
for key in input_keys - output_keys:
87-
ops.append({"op": "remove", "path": ptr.append(key), "key": key})
98+
ops.append({"op": "remove", "path": ptr.append(key)})
99+
rops.insert(0, {"op": "add", "path": ptr.append(key), "value": output[key]})
88100
for key in output_keys - input_keys:
89101
ops.append(
90102
{
91103
"op": "add",
92104
"path": ptr.append(key),
93-
"key": key,
94105
"value": output[key],
95106
}
96107
)
108+
rops.insert(0, {"op": "remove", "path": ptr.append(key)})
97109
for key in input_keys & output_keys:
98-
ops.extend(diff(input[key], output[key], ptr.append(key)))
99-
return ops
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
114+
return ops, rops
100115

101116

102-
def diff_sets(input: Set, output: Set, ptr: Pointer) -> List:
103-
ops = []
117+
def diff_sets(input: Set, output: Set, ptr: Pointer) -> Tuple[List, List]:
118+
ops, rops = [], []
104119
for value in input - output:
105-
ops.append({"op": "remove", "path": ptr.append(value), "value": value})
120+
ops.append({"op": "remove", "path": ptr.append(value)})
121+
rops.insert(0, {"op": "add", "path": ptr.append("-"), "value": value})
106122
for value in output - input:
107-
ops.append({"op": "add", "path": ptr.append(value), "value": value})
108-
return ops
123+
ops.append({"op": "add", "path": ptr.append("-"), "value": value})
124+
rops.insert(0, {"op": "remove", "path": ptr.append(value)})
125+
return ops, rops
109126

110127

111-
def diff(input: Diffable, output: Diffable, ptr: Pointer = None) -> List:
128+
def diff(input: Diffable, output: Diffable, ptr: Pointer = None) -> Tuple[List, List]:
112129
if input == output:
113-
return []
130+
return [], []
114131
if ptr is None:
115132
ptr = Pointer()
116133
if isinstance(input, list) and isinstance(output, list):
@@ -119,4 +136,6 @@ def diff(input: Diffable, output: Diffable, ptr: Pointer = None) -> List:
119136
return diff_dicts(input, output, ptr)
120137
if isinstance(input, set) and isinstance(output, set):
121138
return diff_sets(input, output, ptr)
122-
return [{"op": "replace", "path": ptr, "value": output}]
139+
return [{"op": "replace", "path": ptr, "value": output}], [
140+
{"op": "replace", "path": ptr, "value": input}
141+
]

patchdiff/pointer.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def __str__(self) -> str:
3333
return "/" + "/".join(escape(str(t)) for t in self.tokens)
3434

3535
def __repr__(self) -> str:
36-
return f"Pointer<{str(self)}>"
36+
return f"Pointer({repr(self.tokens)})"
3737

3838
def __hash__(self) -> int:
3939
return hash(self.tokens)
@@ -46,19 +46,19 @@ def __eq__(self, other: "Pointer") -> bool:
4646
def evaluate(self, obj: Diffable) -> Tuple[Diffable, Hashable, Any]:
4747
key = ""
4848
parent = None
49-
value = obj
49+
cursor = obj
5050
for key in self.tokens:
51-
parent = value
51+
parent = cursor
5252
if isinstance(parent, set):
53-
value = key
54-
continue
53+
break
5554
if isinstance(parent, list):
56-
key = int(key)
55+
if key == "-":
56+
break
5757
try:
58-
value = parent[key]
58+
cursor = parent[key]
5959
except KeyError:
6060
break
61-
return parent, key, value
61+
return parent, key, cursor
6262

6363
def append(self, token: Hashable) -> "Pointer":
6464
"""append, creating new Pointer"""

tests/test_apply.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,28 @@
22
from patchdiff.pointer import Pointer
33

44

5-
def test_mixed():
5+
def test_apply():
66
a = {
77
"a": [5, 7, 9, {"a", "b", "c"}],
88
"b": 6,
99
}
1010
b = {"a": [5, 2, 9, {"b", "c"}], "b": 6, "c": 7}
11-
ops = diff(a, b)
11+
ops, rops = diff(a, b)
1212

1313
assert ops == [
14-
{"op": "add", "path": Pointer(["c"]), "key": "c", "value": 7},
14+
{"op": "add", "path": Pointer(["c"]), "value": 7},
1515
{"op": "replace", "path": Pointer(["a", 1]), "value": 2},
16-
{"op": "remove", "path": Pointer(["a", 3, "a"]), "value": "a"},
16+
{"op": "remove", "path": Pointer(["a", 3, "a"])},
17+
]
18+
19+
assert rops == [
20+
{"op": "add", "path": Pointer(["a", 3, "-"]), "value": "a"},
21+
{"op": "replace", "path": Pointer(["a", 1]), "value": 7},
22+
{"op": "remove", "path": Pointer(["c"])},
1723
]
1824

1925
c = apply(a, ops)
2026
assert c == b
27+
28+
d = apply(b, rops)
29+
assert a == d

tests/test_diff.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,29 @@
55
def test_list():
66
a = [1, 5, 9, "sdfsdf", "fff"]
77
b = ["sdf", 5, 9, "c"]
8-
ops = diff(a, b)
8+
ops, rops = diff(a, b)
99

1010
assert ops == [
1111
{"op": "replace", "path": Pointer([0]), "value": "sdf"},
1212
{"op": "replace", "path": Pointer([3]), "value": "c"},
1313
{"op": "remove", "path": Pointer([4])},
1414
]
15+
assert rops == [
16+
{"op": "add", "path": Pointer(["-"]), "value": "fff"},
17+
{"op": "replace", "path": Pointer([4]), "value": "sdfsdf"},
18+
{"op": "replace", "path": Pointer([1]), "value": 1},
19+
]
1520

1621

1722
def test_list_end():
1823
a = [1, 2, 3]
1924
b = [1, 2, 3, 4]
20-
ops = diff(a, b)
25+
ops, rops = diff(a, b)
2126

2227
assert ops == [
2328
{"op": "add", "path": Pointer(["-"]), "value": 4},
2429
]
30+
assert rops == [{"op": "remove", "path": Pointer([3])}]
2531

2632

2733
def test_dicts():
@@ -30,22 +36,30 @@ def test_dicts():
3036
"b": 6,
3137
}
3238
b = {"a": 3, "b": 6, "c": 7}
33-
ops = diff(a, b)
39+
ops, rops = diff(a, b)
3440

3541
assert ops == [
36-
{"op": "add", "path": Pointer(["c"]), "key": "c", "value": 7},
42+
{"op": "add", "path": Pointer(["c"]), "value": 7},
3743
{"op": "replace", "path": Pointer(["a"]), "value": 3},
3844
]
45+
assert rops == [
46+
{"op": "replace", "path": Pointer(["a"]), "value": 5},
47+
{"op": "remove", "path": Pointer(["c"])},
48+
]
3949

4050

4151
def test_sets():
4252
a = {"a", "b"}
4353
b = {"a", "c"}
44-
ops = diff(a, b)
54+
ops, rops = diff(a, b)
4555

4656
assert ops == [
47-
{"op": "remove", "path": Pointer(["b"]), "value": "b"},
48-
{"op": "add", "path": Pointer(["c"]), "value": "c"},
57+
{"op": "remove", "path": Pointer(["b"])},
58+
{"op": "add", "path": Pointer(["-"]), "value": "c"},
59+
]
60+
assert rops == [
61+
{"op": "remove", "path": Pointer(["c"])},
62+
{"op": "add", "path": Pointer(["-"]), "value": "b"},
4963
]
5064

5165

@@ -55,10 +69,15 @@ def test_mixed():
5569
"b": 6,
5670
}
5771
b = {"a": [5, 2, 9, {"b", "c"}], "b": 6, "c": 7}
58-
ops = diff(a, b)
72+
ops, rops = diff(a, b)
5973

6074
assert ops == [
61-
{"op": "add", "path": Pointer(["c"]), "key": "c", "value": 7},
75+
{"op": "add", "path": Pointer(["c"]), "value": 7},
6276
{"op": "replace", "path": Pointer(["a", 1]), "value": 2},
63-
{"op": "remove", "path": Pointer(["a", 3, "a"]), "value": "a"},
77+
{"op": "remove", "path": Pointer(["a", 3, "a"])},
78+
]
79+
assert rops == [
80+
{"op": "add", "path": Pointer(["a", 3, "-"]), "value": "a"},
81+
{"op": "replace", "path": Pointer(["a", 1]), "value": 7},
82+
{"op": "remove", "path": Pointer(["c"])},
6483
]

tests/test_pointer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
def test_pointer_get():
55
obj = [1, 5, {"foo": 1, "bar": [1, 2, 3]}, "sdfsdf", "fff"]
6-
assert Pointer(["1"]).evaluate(obj)[2] == 5
7-
assert Pointer(["2", "bar", "1"]).evaluate(obj)[2] == 2
6+
assert Pointer([1]).evaluate(obj)[2] == 5
7+
assert Pointer([2, "bar", 1]).evaluate(obj)[2] == 2
88

99

1010
def test_pointer_str():
11-
assert str(Pointer(["1"])) == "/1"
11+
assert str(Pointer([1])) == "/1"
1212
assert str(Pointer(["foo", "bar", "-"])) == "/foo/bar/-"

0 commit comments

Comments
 (0)