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
57 changes: 20 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,40 @@
# diff

Diff is a library to calculate deltas between structured data.

## Features

- Calculate detla(s)
- Rebuild state from delta(s)

## Supported Formats

- [x] JSON
- [ ] YAML
- [ ] XML
- [ ] TOML
Calculate RFC 6902 JSON Patch operations between JSON-compatible Python values.
Paths use RFC 6901 JSON Pointer syntax.

## Usage

### Diff

```python
import diff
from diff import diff, patch

old = {"name": "David"}
new = {"name": "Alex"}
deltas = diff.diff(new=new, old=old)

for delta in deltas:
print(delta)
operations = diff(new, old)

assert operations[0].op == "replace"
assert operations[0].path == "/name"
assert operations[0].value == "Alex"
assert patch(old, operations) == new
```

Output

```text
Operation(op='modified', path='$.name', new_value='Alex', old_value='David')
```
The returned `Delta` objects correspond to JSON Patch operation objects and use
the standard `op`, `path`, `value`, and `from_path` fields. `patch` also accepts
ordinary operation dictionaries using RFC names, including `from`.

### Rebuild
Arrays use RFC semantics: `add` inserts, `remove` shifts later elements, and
`replace` updates an existing element. The empty path `""` addresses the
document root.

```python
import diff

old = {"name": "David"}
new = {"name": "Alex"}
deltas = diff.diff(new=new, old=old)

rebuild_new = diff.patch(base=old, deltas=deltas)
## Supported Formats

assert rebuild_new == old
```
- [x] JSON
- [ ] YAML
- [ ] XML
- [ ] TOML

## Install

```bash
poetry add git+https://github.com/includeamin/diff.git#tag
```
```
16 changes: 9 additions & 7 deletions src/diff/delta.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

@dataclasses.dataclass
class Delta:
operation: typing.Literal["deleted", "modified", "added"]
op: typing.Literal["add", "remove", "replace", "move", "copy", "test"]
path: str
new_value: typing.Any | None
old_value: typing.Any | None
value: typing.Any = None
from_path: str | None = None

def __repr__(self):
return (
f"Delta(operation='{self.operation}', path='{self.path}', "
f"new_value={self.new_value!r}, old_value={self.old_value!r})"
)
fields = [f'"op": {self.op!r}', f'"path": {self.path!r}']
if self.op in {"add", "replace", "test"}:
fields.append(f'"value": {self.value!r}')
if self.op in {"move", "copy"}:
fields.append(f'"from": {self.from_path!r}')
return "{" + ", ".join(fields) + "}"
82 changes: 46 additions & 36 deletions src/diff/diff.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,56 @@
import typing
from typing import Any

from diff import json_path
from diff.delta import Delta
from diff.json_path import join_pointer


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
)
operations: list[Delta] = []
def _equal(left: Any, right: Any) -> bool:
if type(left) is not type(right):
return False
if isinstance(left, dict):
return left.keys() == right.keys() and all(
_equal(left[key], right[key]) for key in left
)
if isinstance(left, list):
return len(left) == len(right) and all(
_equal(old_value, new_value)
for old_value, new_value in zip(left, right, strict=True)
)
return left == right

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

def _diff(new: Any, old: Any, path: str, operations: list[Delta]) -> None:
if isinstance(old, dict) and isinstance(new, dict):
operations.extend(
Delta(op="remove", path=join_pointer(path, key))
for key in sorted(old.keys() - new.keys())
)
operations.extend(
Delta(op="add", path=join_pointer(path, key), value=new[key])
for key in sorted(new.keys() - old.keys())
)
for key in sorted(old.keys() & new.keys()):
_diff(new[key], old[key], join_pointer(path, key), operations)
return

added = new_path_map.keys() - old_path_map.keys()
for key in added:
operations.append( # noqa: PERF401
Delta(
path=key, operation="added", old_value=None, new_value=new_path_map[key]
)
if isinstance(old, list) and isinstance(new, list):
for index in range(min(len(old), len(new))):
_diff(new[index], old[index], join_pointer(path, index), operations)
operations.extend(
Delta(op="remove", path=join_pointer(path, index))
for index in range(len(old) - 1, len(new) - 1, -1)
)
operations.extend(
Delta(op="add", path=join_pointer(path, index), value=new[index])
for index in range(len(old), len(new))
)
return

if not _equal(old, new):
operations.append(Delta(op="replace", path=path, value=new))

shared_keys = new_path_map.keys() & old_path_map.keys()
for key in shared_keys:
if old_path_map[key] != new_path_map[key]:
operations.append( # noqa: PERF401
Delta(
path=key,
operation="modified",
old_value=old_path_map[key],
new_value=new_path_map[key],
)
)

def diff(new: Any, old: Any) -> list[Delta]:
operations: list[Delta] = []
_diff(new, old, "", operations)
return operations
Loading
Loading