Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/diff/delta.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

@dataclasses.dataclass
class Delta:
op: typing.Literal["deleted", "modified", "added"]
operation: typing.Literal["deleted", "modified", "added"]
path: str
new_value: typing.Any | None
old_value: typing.Any | None
45 changes: 25 additions & 20 deletions src/diff/diff.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,46 @@
import typing

from diff import json_path
from diff.delta import Delta


def diff(new: dict, old: dict) -> list[Delta]:
def diff(new: dict[str, typing.Any], old: dict[str, typing.Any]) -> list[Delta]:
new_path_map = json_path.path_value_map(
new, include_root=True, leaves_only=True, include_containers=False
)
old_path_map = json_path.path_value_map(
old, include_root=True, leaves_only=False, include_containers=False
)
ops: list[Delta] = []
operations: list[Delta] = []

# deleted
deleted = old_path_map.keys() - new_path_map.keys()
for item in deleted:
ops.append( # noqa: PERF401
Delta(path=item, op="deleted", old_value=old_path_map[item], new_value=None)
for key in deleted:
operations.append( # noqa: PERF401
Delta(
path=key,
operation="deleted",
old_value=old_path_map[key],
new_value=None,
)
)

# added
added = new_path_map.keys() - old_path_map.keys()
for item in added:
ops.append( # noqa: PERF401
Delta(path=item, op="added", old_value=None, new_value=new_path_map[item])
for key in added:
operations.append( # noqa: PERF401
Delta(
path=key, operation="added", old_value=None, new_value=new_path_map[key]
)
)

# modified
shared_keys = new_path_map.keys() & old_path_map.keys()
for item in shared_keys:
if old_path_map[item] != new_path_map[item]:
ops.append( # noqa: PERF401
for key in shared_keys:
if old_path_map[key] != new_path_map[key]:
operations.append( # noqa: PERF401
Delta(
path=item,
op="modified",
old_value=old_path_map[item],
new_value=new_path_map[item],
path=key,
operation="modified",
old_value=old_path_map[key],
new_value=new_path_map[key],
)
)
# exlude root diff
return ops
return operations
3 changes: 2 additions & 1 deletion src/diff/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,10 @@ def pop_by_json_path(
def patch(base: dict[str, typing.Any], deltas: list[Delta]) -> dict[str, typing.Any]:
output = copy.deepcopy(base)
for op in deltas:
if op.op == "deleted":
if op.operation == "deleted":
pop_by_json_path(output, op.path, prune_empty=True, remove_from_list=True)
continue
# we use set for both modify and add actions
set_by_json_path(
output,
op.path,
Expand Down
19 changes: 14 additions & 5 deletions tests/test_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ def _mk_expected_operation(**kwargs):
(
{"name": "Amin"},
{"name": "Amin2"},
[Delta(op="modified", path="$.name", old_value="Amin", new_value="Amin2")],
[
Delta(
operation="modified",
path="$.name",
old_value="Amin",
new_value="Amin2",
)
],
)
],
)
Expand Down Expand Up @@ -76,7 +83,9 @@ def test_added_key_expected_op_and_patch_roundtrip():

# At least one 'added' operation to $.age (value semantics may differ: new_value vs value)
assert any(
o.op == "added" and o.path == "$.age" and _op_get(o, "new_value", "value") == 30
o.operation == "added"
and o.path == "$.age"
and _op_get(o, "new_value", "value") == 30
for o in ops
)

Expand All @@ -91,7 +100,7 @@ def test_deleted_key_expected_op_and_patch_roundtrip():

# At least one 'deleted' operation from $.name (old_value/value semantics)
assert any(
o.op == "deleted"
o.operation == "deleted"
and o.path == "$.name"
and _op_get(o, "old_value", "value") == "Amin"
for o in ops
Expand Down Expand Up @@ -151,7 +160,7 @@ def test_none_handling_as_value_vs_absence():
old2 = {"y": None}
new2: dict[str, typing.Any] = {}
ops2 = diff(new=new2, old=old2)
assert any(o.op == "deleted" and o.path == "$.y" for o in ops2)
assert any(o.operation == "deleted" and o.path == "$.y" for o in ops2)
assert patch(base=old2, deltas=ops2) == new2


Expand All @@ -176,7 +185,7 @@ def test_multiple_changes_in_one_structure():
"$.user.role": "added",
"$.settings.theme": "modified",
}
seen = {o.path: o.op for o in ops if o.path in expect_paths}
seen = {o.path: o.operation for o in ops if o.path in expect_paths}
for p, expected_op in expect_paths.items():
assert seen.get(p) == expected_op, (
f"Expected {expected_op} at {p}, got {seen.get(p)}"
Expand Down