patchdiff 0.3.3__tar.gz → 0.3.5__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.
- patchdiff-0.3.5/.github/workflows/ci.yml +103 -0
- patchdiff-0.3.5/.gitignore +6 -0
- {patchdiff-0.3.3 → patchdiff-0.3.5}/PKG-INFO +6 -13
- {patchdiff-0.3.3 → patchdiff-0.3.5}/README.md +1 -1
- {patchdiff-0.3.3 → patchdiff-0.3.5}/patchdiff/__init__.py +1 -1
- {patchdiff-0.3.3 → patchdiff-0.3.5}/patchdiff/diff.py +3 -3
- {patchdiff-0.3.3 → patchdiff-0.3.5}/patchdiff/pointer.py +7 -6
- patchdiff-0.3.5/patchdiff/types.py +3 -0
- patchdiff-0.3.5/pyproject.toml +38 -0
- patchdiff-0.3.5/tests/test_apply.py +111 -0
- patchdiff-0.3.5/tests/test_diff.py +170 -0
- patchdiff-0.3.5/tests/test_pointer.py +46 -0
- patchdiff-0.3.5/tests/test_proxy.py +86 -0
- patchdiff-0.3.5/tests/test_serialize.py +31 -0
- patchdiff-0.3.3/patchdiff/types.py +0 -3
- patchdiff-0.3.3/pyproject.toml +0 -23
- patchdiff-0.3.3/setup.py +0 -26
- {patchdiff-0.3.3 → patchdiff-0.3.5}/patchdiff/apply.py +0 -0
- {patchdiff-0.3.3 → patchdiff-0.3.5}/patchdiff/serialize.py +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
tags:
|
|
8
|
+
- 'v*'
|
|
9
|
+
pull_request:
|
|
10
|
+
branches:
|
|
11
|
+
- master
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
name: Lint and test on ${{ matrix.name }}
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
include:
|
|
21
|
+
- name: Linux py38
|
|
22
|
+
pyversion: '3.8'
|
|
23
|
+
- name: Linux py39
|
|
24
|
+
pyversion: '3.9'
|
|
25
|
+
- name: Linux py310
|
|
26
|
+
pyversion: '3.10'
|
|
27
|
+
- name: Linux py311
|
|
28
|
+
pyversion: '3.11'
|
|
29
|
+
- name: Linux py312
|
|
30
|
+
pyversion: '3.12'
|
|
31
|
+
- name: Linux py313
|
|
32
|
+
pyversion: '3.13'
|
|
33
|
+
- name: Linux py314
|
|
34
|
+
pyversion: '3.14'
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v5
|
|
37
|
+
- name: Install uv
|
|
38
|
+
uses: astral-sh/setup-uv@v6
|
|
39
|
+
- name: Set up Python ${{ matrix.pyversion }}
|
|
40
|
+
uses: actions/setup-python@v5
|
|
41
|
+
with:
|
|
42
|
+
python-version: ${{ matrix.pyversion }}
|
|
43
|
+
- name: Install dependencies
|
|
44
|
+
run: uv sync
|
|
45
|
+
- name: Lint
|
|
46
|
+
run: uv run ruff check
|
|
47
|
+
- name: Format
|
|
48
|
+
run: uv run ruff format --check
|
|
49
|
+
- name: Test
|
|
50
|
+
run: uv run pytest -v --cov=patchdiff --cov-report=term-missing
|
|
51
|
+
|
|
52
|
+
build:
|
|
53
|
+
name: Build and test wheel
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v5
|
|
57
|
+
- name: Install uv
|
|
58
|
+
uses: astral-sh/setup-uv@v6
|
|
59
|
+
- name: Set up Python 3.9
|
|
60
|
+
uses: actions/setup-python@v5
|
|
61
|
+
with:
|
|
62
|
+
python-version: '3.9'
|
|
63
|
+
- name: Install dependencies
|
|
64
|
+
run: uv sync
|
|
65
|
+
- name: Build wheel
|
|
66
|
+
run: uv build
|
|
67
|
+
- name: Twine check
|
|
68
|
+
run: uvx twine check dist/*
|
|
69
|
+
- name: Upload wheel artifact
|
|
70
|
+
uses: actions/upload-artifact@v4
|
|
71
|
+
with:
|
|
72
|
+
path: dist
|
|
73
|
+
name: dist
|
|
74
|
+
|
|
75
|
+
publish:
|
|
76
|
+
name: Publish to Github and Pypi
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
needs: [test, build]
|
|
79
|
+
if: success() && startsWith(github.ref, 'refs/tags/v')
|
|
80
|
+
environment:
|
|
81
|
+
name: pypi
|
|
82
|
+
url: https://pypi.org/p/patchdiff
|
|
83
|
+
permissions:
|
|
84
|
+
id-token: write
|
|
85
|
+
contents: write
|
|
86
|
+
steps:
|
|
87
|
+
- uses: actions/checkout@v5
|
|
88
|
+
- name: Download wheel artifact
|
|
89
|
+
uses: actions/download-artifact@v4
|
|
90
|
+
with:
|
|
91
|
+
name: dist
|
|
92
|
+
path: dist
|
|
93
|
+
- name: Release
|
|
94
|
+
uses: softprops/action-gh-release@v2
|
|
95
|
+
with:
|
|
96
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
97
|
+
files: |
|
|
98
|
+
dist/*.tar.gz
|
|
99
|
+
dist/*.whl
|
|
100
|
+
draft: true
|
|
101
|
+
prerelease: false
|
|
102
|
+
- name: Publish to PyPI
|
|
103
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: patchdiff
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: MIT
|
|
5
|
-
|
|
6
|
-
Author: Korijn van Golen
|
|
7
|
-
|
|
8
|
-
Requires-Python: >=3.7
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
5
|
+
Project-URL: Homepage, https://github.com/fork-tongue/patchdiff
|
|
6
|
+
Author-email: Korijn van Golen <korijn@gmail.com>, Berend Klein Haneveld <berendkleinhaneveld@gmail.com>
|
|
7
|
+
Requires-Python: >=3.8
|
|
14
8
|
Description-Content-Type: text/markdown
|
|
15
9
|
|
|
16
10
|
[](https://badge.fury.io/py/patchdiff)
|
|
17
|
-
[](https://github.com/fork-tongue/patchdiff/actions)
|
|
18
12
|
|
|
19
13
|
# Patchdiff 🔍
|
|
20
14
|
|
|
@@ -58,4 +52,3 @@ print(to_json(ops, indent=4))
|
|
|
58
52
|
# }
|
|
59
53
|
# ]
|
|
60
54
|
```
|
|
61
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[](https://badge.fury.io/py/patchdiff)
|
|
2
|
-
[](https://github.com/fork-tongue/patchdiff/actions)
|
|
3
3
|
|
|
4
4
|
# Patchdiff 🔍
|
|
5
5
|
|
|
@@ -71,13 +71,13 @@ def diff_lists(input: List, output: List, ptr: Pointer) -> Tuple[List, List]:
|
|
|
71
71
|
"path": ptr.append(idx_token),
|
|
72
72
|
"value": op["value"],
|
|
73
73
|
}
|
|
74
|
-
return [ops
|
|
74
|
+
return [[*ops, full_op], padding + 1]
|
|
75
75
|
elif op["op"] == "remove":
|
|
76
76
|
full_op = {
|
|
77
77
|
"op": "remove",
|
|
78
78
|
"path": ptr.append(op["idx"] + padding),
|
|
79
79
|
}
|
|
80
|
-
return [ops
|
|
80
|
+
return [[*ops, full_op], padding - 1]
|
|
81
81
|
else:
|
|
82
82
|
replace_ptr = ptr.append(op["idx"] + padding)
|
|
83
83
|
replace_ops, _ = diff(op["original"], op["value"], replace_ptr)
|
|
@@ -96,7 +96,7 @@ def diff_dicts(input: Dict, output: Dict, ptr: Pointer) -> Tuple[List, List]:
|
|
|
96
96
|
output_keys = set(output.keys())
|
|
97
97
|
for key in input_keys - output_keys:
|
|
98
98
|
ops.append({"op": "remove", "path": ptr.append(key)})
|
|
99
|
-
rops.insert(0, {"op": "add", "path": ptr.append(key), "value":
|
|
99
|
+
rops.insert(0, {"op": "add", "path": ptr.append(key), "value": input[key]})
|
|
100
100
|
for key in output_keys - input_keys:
|
|
101
101
|
ops.append(
|
|
102
102
|
{
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import re
|
|
2
4
|
from typing import Any, Hashable, List, Tuple
|
|
3
5
|
|
|
4
6
|
from .types import Diffable
|
|
5
7
|
|
|
6
|
-
|
|
7
8
|
tilde0_re = re.compile("~0")
|
|
8
9
|
tilde1_re = re.compile("~1")
|
|
9
10
|
tilde_re = re.compile("~")
|
|
@@ -19,21 +20,21 @@ def escape(token: str) -> str:
|
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class Pointer:
|
|
22
|
-
def __init__(self, tokens: List[Hashable] = None) -> None:
|
|
23
|
+
def __init__(self, tokens: List[Hashable] | None = None) -> None:
|
|
23
24
|
if tokens is None:
|
|
24
25
|
tokens = []
|
|
25
|
-
self.tokens = tokens
|
|
26
|
+
self.tokens = tuple(tokens)
|
|
26
27
|
|
|
27
28
|
@staticmethod
|
|
28
29
|
def from_str(path: str) -> "Pointer":
|
|
29
|
-
tokens = [unescape(t) for t in path.split("/")]
|
|
30
|
+
tokens = [unescape(t) for t in path.split("/")[1:]]
|
|
30
31
|
return Pointer(tokens)
|
|
31
32
|
|
|
32
33
|
def __str__(self) -> str:
|
|
33
34
|
return "/" + "/".join(escape(str(t)) for t in self.tokens)
|
|
34
35
|
|
|
35
36
|
def __repr__(self) -> str:
|
|
36
|
-
return f"Pointer({
|
|
37
|
+
return f"Pointer({list(self.tokens)!r})"
|
|
37
38
|
|
|
38
39
|
def __hash__(self) -> int:
|
|
39
40
|
return hash(self.tokens)
|
|
@@ -62,4 +63,4 @@ class Pointer:
|
|
|
62
63
|
|
|
63
64
|
def append(self, token: Hashable) -> "Pointer":
|
|
64
65
|
"""append, creating new Pointer"""
|
|
65
|
-
return Pointer(self.tokens
|
|
66
|
+
return Pointer((*self.tokens, token))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "patchdiff"
|
|
3
|
+
version = "0.3.5"
|
|
4
|
+
description = "MIT"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Korijn van Golen", email = "korijn@gmail.com" },
|
|
7
|
+
{ name = "Berend Klein Haneveld", email = "berendkleinhaneveld@gmail.com" },
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Homepage = "https://github.com/fork-tongue/patchdiff"
|
|
14
|
+
|
|
15
|
+
[dependency-groups]
|
|
16
|
+
dev = [
|
|
17
|
+
"ruff",
|
|
18
|
+
"pytest",
|
|
19
|
+
"pytest-cov",
|
|
20
|
+
"pytest-watch",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.ruff.lint]
|
|
24
|
+
extend-select = [
|
|
25
|
+
"F", # Pyflakes (default)
|
|
26
|
+
"I", # isort imports
|
|
27
|
+
"N", # pep8-naming
|
|
28
|
+
"T10", # flake8-debugger
|
|
29
|
+
"T20", # flake8-print
|
|
30
|
+
"RUF", # ruff
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.ruff.lint.per-file-ignores]
|
|
34
|
+
"patchdiff/__init__.py" = ["F401"]
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from patchdiff import apply, diff
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_apply():
|
|
5
|
+
a = {
|
|
6
|
+
"a": [5, 7, 9, {"a", "b", "c"}],
|
|
7
|
+
"b": 6,
|
|
8
|
+
}
|
|
9
|
+
b = {
|
|
10
|
+
"a": [5, 2, 9, {"b", "c"}],
|
|
11
|
+
"b": 6,
|
|
12
|
+
"c": 7,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ops, rops = diff(a, b)
|
|
16
|
+
|
|
17
|
+
c = apply(a, ops)
|
|
18
|
+
assert c == b
|
|
19
|
+
|
|
20
|
+
d = apply(b, rops)
|
|
21
|
+
assert a == d
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_apply_list():
|
|
25
|
+
a = [1, 5, 9, "sdfsdf", "fff"]
|
|
26
|
+
b = ["sdf", 5, 9, "c"]
|
|
27
|
+
|
|
28
|
+
ops, rops = diff(a, b)
|
|
29
|
+
|
|
30
|
+
c = apply(a, ops)
|
|
31
|
+
assert c == b
|
|
32
|
+
|
|
33
|
+
d = apply(b, rops)
|
|
34
|
+
assert a == d
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_add_remove_list():
|
|
38
|
+
a = []
|
|
39
|
+
b = [1]
|
|
40
|
+
|
|
41
|
+
ops, rops = diff(a, b)
|
|
42
|
+
|
|
43
|
+
c = apply(a, ops)
|
|
44
|
+
assert c == b
|
|
45
|
+
|
|
46
|
+
d = apply(b, rops)
|
|
47
|
+
assert a == d
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_add_remove_list_extended():
|
|
51
|
+
a = []
|
|
52
|
+
b = [1, 2, 3]
|
|
53
|
+
|
|
54
|
+
ops, rops = diff(a, b)
|
|
55
|
+
|
|
56
|
+
c = apply(a, ops)
|
|
57
|
+
assert c == b
|
|
58
|
+
|
|
59
|
+
d = apply(b, rops)
|
|
60
|
+
assert a == d
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_insertion_in_list_front():
|
|
64
|
+
a = [1, 2]
|
|
65
|
+
b = [3, 1, 2]
|
|
66
|
+
ops, rops = diff(a, b)
|
|
67
|
+
|
|
68
|
+
c = apply(a, ops)
|
|
69
|
+
assert c == b
|
|
70
|
+
|
|
71
|
+
d = apply(b, rops)
|
|
72
|
+
assert a == d
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_add_remove_list_extended_inverse():
|
|
76
|
+
a = [1, 2, 3]
|
|
77
|
+
b = []
|
|
78
|
+
|
|
79
|
+
ops, rops = diff(a, b)
|
|
80
|
+
|
|
81
|
+
c = apply(a, ops)
|
|
82
|
+
assert c == b
|
|
83
|
+
|
|
84
|
+
d = apply(b, rops)
|
|
85
|
+
assert a == d
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_add_remove_list_extended_inverse_leaving_start():
|
|
89
|
+
a = [1, 2, 3, 4]
|
|
90
|
+
b = [1]
|
|
91
|
+
|
|
92
|
+
ops, rops = diff(a, b)
|
|
93
|
+
|
|
94
|
+
c = apply(a, ops)
|
|
95
|
+
assert c == b
|
|
96
|
+
|
|
97
|
+
d = apply(b, rops)
|
|
98
|
+
assert a == d
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_add_remove_list_extended_inverse_leaving_end():
|
|
102
|
+
a = [1, 2, 3, 4]
|
|
103
|
+
b = [4]
|
|
104
|
+
|
|
105
|
+
ops, rops = diff(a, b)
|
|
106
|
+
|
|
107
|
+
c = apply(a, ops)
|
|
108
|
+
assert c == b
|
|
109
|
+
|
|
110
|
+
d = apply(b, rops)
|
|
111
|
+
assert a == d
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from patchdiff import diff
|
|
2
|
+
from patchdiff.pointer import Pointer
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_basic_list_insertion():
|
|
6
|
+
a = []
|
|
7
|
+
b = [1]
|
|
8
|
+
ops, rops = diff(a, b)
|
|
9
|
+
|
|
10
|
+
assert ops == [{"op": "add", "path": Pointer(["-"]), "value": 1}]
|
|
11
|
+
assert rops == [{"op": "remove", "path": Pointer([0])}]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_basic_list_deletion():
|
|
15
|
+
a = [1]
|
|
16
|
+
b = []
|
|
17
|
+
ops, rops = diff(a, b)
|
|
18
|
+
|
|
19
|
+
assert ops == [{"op": "remove", "path": Pointer([0])}]
|
|
20
|
+
assert rops == [{"op": "add", "path": Pointer(["-"]), "value": 1}]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_basic_list_insertion_half_way():
|
|
24
|
+
a = [1, 3]
|
|
25
|
+
b = [1, 2, 3]
|
|
26
|
+
ops, rops = diff(a, b)
|
|
27
|
+
|
|
28
|
+
assert ops == [{"op": "add", "path": Pointer([1]), "value": 2}]
|
|
29
|
+
assert rops == [{"op": "remove", "path": Pointer([1])}]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_basic_list_deletion_half_way():
|
|
33
|
+
a = [1, 2, 3]
|
|
34
|
+
b = [1, 3]
|
|
35
|
+
ops, rops = diff(a, b)
|
|
36
|
+
|
|
37
|
+
assert ops == [{"op": "remove", "path": Pointer([1])}]
|
|
38
|
+
assert rops == [{"op": "add", "path": Pointer([1]), "value": 2}]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_basic_list_multiple_insertion():
|
|
42
|
+
a = []
|
|
43
|
+
b = [1, 2, 3]
|
|
44
|
+
ops, rops = diff(a, b)
|
|
45
|
+
|
|
46
|
+
assert ops == [
|
|
47
|
+
{"op": "add", "path": Pointer(["-"]), "value": 1},
|
|
48
|
+
{"op": "add", "path": Pointer(["-"]), "value": 2},
|
|
49
|
+
{"op": "add", "path": Pointer(["-"]), "value": 3},
|
|
50
|
+
]
|
|
51
|
+
assert rops == [
|
|
52
|
+
{"op": "remove", "path": Pointer([0])},
|
|
53
|
+
{"op": "remove", "path": Pointer([0])},
|
|
54
|
+
{"op": "remove", "path": Pointer([0])},
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_basic_list_multiple_deletion():
|
|
59
|
+
a = [1, 2, 3]
|
|
60
|
+
b = []
|
|
61
|
+
ops, rops = diff(a, b)
|
|
62
|
+
|
|
63
|
+
assert ops == [
|
|
64
|
+
{"op": "remove", "path": Pointer([0])},
|
|
65
|
+
{"op": "remove", "path": Pointer([0])},
|
|
66
|
+
{"op": "remove", "path": Pointer([0])},
|
|
67
|
+
]
|
|
68
|
+
assert rops == [
|
|
69
|
+
{"op": "add", "path": Pointer(["-"]), "value": 1},
|
|
70
|
+
{"op": "add", "path": Pointer(["-"]), "value": 2},
|
|
71
|
+
{"op": "add", "path": Pointer(["-"]), "value": 3},
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_list():
|
|
76
|
+
a = [1, 5, 9, "sdfsdf", "fff"]
|
|
77
|
+
b = ["sdf", 5, 9, "c"]
|
|
78
|
+
ops, rops = diff(a, b)
|
|
79
|
+
|
|
80
|
+
assert ops == [
|
|
81
|
+
{"op": "replace", "path": Pointer([0]), "value": "sdf"},
|
|
82
|
+
{"op": "replace", "path": Pointer([3]), "value": "c"},
|
|
83
|
+
{"op": "remove", "path": Pointer([4])},
|
|
84
|
+
]
|
|
85
|
+
assert rops == [
|
|
86
|
+
{"op": "replace", "path": Pointer([0]), "value": 1},
|
|
87
|
+
{"op": "replace", "path": Pointer([3]), "value": "sdfsdf"},
|
|
88
|
+
{"op": "add", "path": Pointer(["-"]), "value": "fff"},
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_list_begin():
|
|
93
|
+
a = [1, 2]
|
|
94
|
+
b = [3, 1, 2]
|
|
95
|
+
ops, rops = diff(a, b)
|
|
96
|
+
|
|
97
|
+
assert ops == [{"op": "add", "path": Pointer([0]), "value": 3}]
|
|
98
|
+
assert rops == [{"op": "remove", "path": Pointer([0])}]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_list_end():
|
|
102
|
+
a = [1, 2, 3]
|
|
103
|
+
b = [1, 2, 3, 4]
|
|
104
|
+
ops, rops = diff(a, b)
|
|
105
|
+
|
|
106
|
+
assert ops == [{"op": "add", "path": Pointer(["-"]), "value": 4}]
|
|
107
|
+
assert rops == [{"op": "remove", "path": Pointer([3])}]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_dicts():
|
|
111
|
+
a = {"a": 5, "b": 6}
|
|
112
|
+
b = {"a": 3, "b": 6, "c": 7}
|
|
113
|
+
ops, rops = diff(a, b)
|
|
114
|
+
|
|
115
|
+
assert ops == [
|
|
116
|
+
{"op": "add", "path": Pointer(["c"]), "value": 7},
|
|
117
|
+
{"op": "replace", "path": Pointer(["a"]), "value": 3},
|
|
118
|
+
]
|
|
119
|
+
assert rops == [
|
|
120
|
+
{"op": "replace", "path": Pointer(["a"]), "value": 5},
|
|
121
|
+
{"op": "remove", "path": Pointer(["c"])},
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_dicts_remove_item():
|
|
126
|
+
a = {"a": 3, "b": 6}
|
|
127
|
+
b = {"a": 3}
|
|
128
|
+
ops, rops = diff(a, b)
|
|
129
|
+
|
|
130
|
+
assert ops == [{"op": "remove", "path": Pointer(["b"])}]
|
|
131
|
+
assert rops == [{"op": "add", "path": Pointer(["b"]), "value": 6}]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_sets():
|
|
135
|
+
a = {"a", "b"}
|
|
136
|
+
b = {"a", "c"}
|
|
137
|
+
ops, rops = diff(a, b)
|
|
138
|
+
|
|
139
|
+
assert ops == [
|
|
140
|
+
{"op": "remove", "path": Pointer(["b"])},
|
|
141
|
+
{"op": "add", "path": Pointer(["-"]), "value": "c"},
|
|
142
|
+
]
|
|
143
|
+
assert rops == [
|
|
144
|
+
{"op": "remove", "path": Pointer(["c"])},
|
|
145
|
+
{"op": "add", "path": Pointer(["-"]), "value": "b"},
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_mixed():
|
|
150
|
+
a = {
|
|
151
|
+
"a": [5, 7, 9, {"a", "b", "c"}],
|
|
152
|
+
"b": 6,
|
|
153
|
+
}
|
|
154
|
+
b = {
|
|
155
|
+
"a": [5, 2, 9, {"b", "c"}],
|
|
156
|
+
"b": 6,
|
|
157
|
+
"c": 7,
|
|
158
|
+
}
|
|
159
|
+
ops, rops = diff(a, b)
|
|
160
|
+
|
|
161
|
+
assert ops == [
|
|
162
|
+
{"op": "add", "path": Pointer(["c"]), "value": 7},
|
|
163
|
+
{"op": "replace", "path": Pointer(["a", 1]), "value": 2},
|
|
164
|
+
{"op": "remove", "path": Pointer(["a", 3, "a"])},
|
|
165
|
+
]
|
|
166
|
+
assert rops == [
|
|
167
|
+
{"op": "replace", "path": Pointer(["a", 1]), "value": 7},
|
|
168
|
+
{"op": "add", "path": Pointer(["a", 3, "-"]), "value": "a"},
|
|
169
|
+
{"op": "remove", "path": Pointer(["c"])},
|
|
170
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from patchdiff.pointer import Pointer
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_pointer_get():
|
|
5
|
+
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
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_pointer_str():
|
|
11
|
+
assert str(Pointer([1])) == "/1"
|
|
12
|
+
assert str(Pointer(["foo", "bar", "-"])) == "/foo/bar/-"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_pointer_repr():
|
|
16
|
+
assert repr(Pointer([1])) == "Pointer([1])"
|
|
17
|
+
assert repr(Pointer(["foo", "bar", "-"])) == "Pointer(['foo', 'bar', '-'])"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_pointer_from_str():
|
|
21
|
+
assert Pointer.from_str("/1") == Pointer(["1"])
|
|
22
|
+
assert Pointer.from_str("/foo/bar/-") == Pointer(["foo", "bar", "-"])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_pointer_hash():
|
|
26
|
+
assert hash(Pointer([1])) == hash((1,))
|
|
27
|
+
assert hash(Pointer(["foo", "bar", "-"])) == hash(("foo", "bar", "-"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_pointer_set():
|
|
31
|
+
# hash supports comparison operators for use as keys and set elements
|
|
32
|
+
# so we exercise that as well
|
|
33
|
+
unique_pointers = [Pointer([1]), Pointer(["2", "3"])]
|
|
34
|
+
duplicated_pointers = unique_pointers + unique_pointers
|
|
35
|
+
assert len(set(duplicated_pointers)) == len(unique_pointers)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_pointer_eq():
|
|
39
|
+
assert Pointer([1]) != [1]
|
|
40
|
+
assert Pointer([1]) != Pointer(["1"])
|
|
41
|
+
assert Pointer([1]) != Pointer([0])
|
|
42
|
+
assert Pointer([1]) == Pointer([1])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_pointer_append():
|
|
46
|
+
assert Pointer([1]).append("foo") == Pointer([1, "foo"])
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests that check that patchdiff apply and diff work
|
|
3
|
+
on proxied objects.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections import UserDict, UserList
|
|
7
|
+
from collections.abc import Set
|
|
8
|
+
|
|
9
|
+
from patchdiff import apply, diff
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SetProxy(Set):
|
|
13
|
+
"""
|
|
14
|
+
Custom proxy class that works like UserDict and UserList by
|
|
15
|
+
storing the original data under the 'data' attribute.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, *args, **kwargs):
|
|
19
|
+
self.data = set(*args, **kwargs)
|
|
20
|
+
|
|
21
|
+
def __contains__(self, *args, **kwargs):
|
|
22
|
+
return self.data.__contains__(*args, **kwargs)
|
|
23
|
+
|
|
24
|
+
def __iter__(self, *args, **kwargs):
|
|
25
|
+
return self.data.__iter__(*args, **kwargs)
|
|
26
|
+
|
|
27
|
+
def __len__(self, *args, **kwargs):
|
|
28
|
+
return self.data.__len__(*args, **kwargs)
|
|
29
|
+
|
|
30
|
+
def __getattribute__(self, attr):
|
|
31
|
+
# Redirect __class__ to super() to make sure
|
|
32
|
+
# isinstance(obj, set) will fail
|
|
33
|
+
if attr in {"data", "_from_iterable", "__class__"}:
|
|
34
|
+
return super().__getattribute__(attr)
|
|
35
|
+
return self.data.__getattribute__(attr)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_proxy_dict():
|
|
39
|
+
data = {"foo": "bar"}
|
|
40
|
+
obj = UserDict(data)
|
|
41
|
+
assert not isinstance(obj, dict)
|
|
42
|
+
|
|
43
|
+
old = obj.copy()
|
|
44
|
+
obj["foo"] = "baz"
|
|
45
|
+
|
|
46
|
+
assert old["foo"] == "bar"
|
|
47
|
+
assert obj["foo"] == "baz"
|
|
48
|
+
|
|
49
|
+
ops, reverse_ops = diff(old, obj)
|
|
50
|
+
|
|
51
|
+
assert apply(old, ops) == obj
|
|
52
|
+
assert apply(obj, reverse_ops) == old
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_proxy_list():
|
|
56
|
+
data = [1, 2]
|
|
57
|
+
obj = UserList(data)
|
|
58
|
+
assert not isinstance(obj, list)
|
|
59
|
+
|
|
60
|
+
old = obj.copy()
|
|
61
|
+
obj[1] = 3
|
|
62
|
+
|
|
63
|
+
assert old[1] == 2
|
|
64
|
+
assert obj[1] == 3
|
|
65
|
+
|
|
66
|
+
ops, reverse_ops = diff(old, obj)
|
|
67
|
+
|
|
68
|
+
assert apply(old, ops) == obj
|
|
69
|
+
assert apply(obj, reverse_ops) == old
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_proxy_set():
|
|
73
|
+
data = {"a", "b"}
|
|
74
|
+
obj = SetProxy(data)
|
|
75
|
+
assert not isinstance(obj, set)
|
|
76
|
+
|
|
77
|
+
old = obj.copy()
|
|
78
|
+
obj.add("c")
|
|
79
|
+
|
|
80
|
+
assert "c" not in old
|
|
81
|
+
assert "c" in obj
|
|
82
|
+
|
|
83
|
+
ops, reverse_ops = diff(old, obj)
|
|
84
|
+
|
|
85
|
+
assert apply(old, ops) == obj
|
|
86
|
+
assert apply(obj, reverse_ops) == old
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from patchdiff import diff, to_json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_to_json():
|
|
5
|
+
a = {
|
|
6
|
+
"a": [5, 7, 9, {"a", "b", "c"}],
|
|
7
|
+
"b": 6,
|
|
8
|
+
}
|
|
9
|
+
b = {"a": [5, 2, 9, {"b", "c"}], "b": 6, "c": 7}
|
|
10
|
+
|
|
11
|
+
ops, _ = diff(a, b)
|
|
12
|
+
|
|
13
|
+
assert (
|
|
14
|
+
to_json(ops, indent=4)
|
|
15
|
+
== """[
|
|
16
|
+
{
|
|
17
|
+
"op": "add",
|
|
18
|
+
"path": "/c",
|
|
19
|
+
"value": 7
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"op": "replace",
|
|
23
|
+
"path": "/a/1",
|
|
24
|
+
"value": 2
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"op": "remove",
|
|
28
|
+
"path": "/a/3/a"
|
|
29
|
+
}
|
|
30
|
+
]"""
|
|
31
|
+
)
|
patchdiff-0.3.3/pyproject.toml
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "patchdiff"
|
|
3
|
-
version = "0.3.3"
|
|
4
|
-
description = "MIT"
|
|
5
|
-
authors = ["Korijn van Golen <korijn@gmail.com>"]
|
|
6
|
-
homepage = "https://github.com/Korijn/patchdiff"
|
|
7
|
-
readme = "README.md"
|
|
8
|
-
|
|
9
|
-
[tool.poetry.dependencies]
|
|
10
|
-
python = ">=3.7"
|
|
11
|
-
|
|
12
|
-
[tool.poetry.dev-dependencies]
|
|
13
|
-
flake8 = "*"
|
|
14
|
-
black = "*"
|
|
15
|
-
flake8-black = "*"
|
|
16
|
-
flake8-import-order = "*"
|
|
17
|
-
flake8-print = "*"
|
|
18
|
-
pytest = "*"
|
|
19
|
-
twine = "*"
|
|
20
|
-
|
|
21
|
-
[build-system]
|
|
22
|
-
requires = ["poetry-core>=1.0.0"]
|
|
23
|
-
build-backend = "poetry.core.masonry.api"
|
patchdiff-0.3.3/setup.py
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
from setuptools import setup
|
|
3
|
-
|
|
4
|
-
packages = \
|
|
5
|
-
['patchdiff']
|
|
6
|
-
|
|
7
|
-
package_data = \
|
|
8
|
-
{'': ['*']}
|
|
9
|
-
|
|
10
|
-
setup_kwargs = {
|
|
11
|
-
'name': 'patchdiff',
|
|
12
|
-
'version': '0.3.3',
|
|
13
|
-
'description': 'MIT',
|
|
14
|
-
'long_description': '[](https://badge.fury.io/py/patchdiff)\n[](https://github.com/Korijn/patchdiff/actions)\n\n# Patchdiff 🔍\n\nBased on [rfc6902](https://github.com/chbrown/rfc6902) this library provides a simple API to generate bi-directional diffs between composite python datastructures composed out of lists, sets, tuples and dicts. The diffs are jsonpatch compliant, and can optionally be serialized to json format. Patchdiff can also be used to apply lists of patches to objects, both in-place or on a deepcopy of the input.\n\n## Install\n\n`pip install patchdiff`\n\n## Quick-start\n\n```python\nfrom patchdiff import apply, diff, iapply, to_json\n\ninput = {"a": [5, 7, 9, {"a", "b", "c"}], "b": 6}\noutput = {"a": [5, 2, 9, {"b", "c"}], "b": 6, "c": 7}\n\nops, reverse_ops = diff(input, output)\n\nassert apply(input, ops) == output\nassert apply(output, reverse_ops) == input\n\niapply(input, ops) # apply in-place\nassert input == output\n\nprint(to_json(ops, indent=4))\n# [\n# {\n# "op": "add",\n# "path": "/c",\n# "value": 7\n# },\n# {\n# "op": "replace",\n# "path": "/a/1",\n# "value": 2\n# },\n# {\n# "op": "remove",\n# "path": "/a/3/a"\n# }\n# ]\n```\n',
|
|
15
|
-
'author': 'Korijn van Golen',
|
|
16
|
-
'author_email': 'korijn@gmail.com',
|
|
17
|
-
'maintainer': None,
|
|
18
|
-
'maintainer_email': None,
|
|
19
|
-
'url': 'https://github.com/Korijn/patchdiff',
|
|
20
|
-
'packages': packages,
|
|
21
|
-
'package_data': package_data,
|
|
22
|
-
'python_requires': '>=3.7',
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
setup(**setup_kwargs)
|
|
File without changes
|
|
File without changes
|