pl-dict-diff 0.0.1__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.
- pl_dict_diff-0.0.1/LICENSE +28 -0
- pl_dict_diff-0.0.1/PKG-INFO +125 -0
- pl_dict_diff-0.0.1/README.md +95 -0
- pl_dict_diff-0.0.1/inspire_dictdiffer/__init__.py +430 -0
- pl_dict_diff-0.0.1/inspire_dictdiffer/conflict.py +80 -0
- pl_dict_diff-0.0.1/inspire_dictdiffer/merge.py +163 -0
- pl_dict_diff-0.0.1/inspire_dictdiffer/resolve.py +153 -0
- pl_dict_diff-0.0.1/inspire_dictdiffer/testing.py +36 -0
- pl_dict_diff-0.0.1/inspire_dictdiffer/unify.py +53 -0
- pl_dict_diff-0.0.1/inspire_dictdiffer/utils.py +310 -0
- pl_dict_diff-0.0.1/inspire_dictdiffer/version.py +4 -0
- pl_dict_diff-0.0.1/pyproject.toml +38 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Dictdiffer is free software; you can redistribute it and/or modify it
|
|
2
|
+
under the terms of the MIT License quoted below.
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2013 Fatih Erikli.
|
|
5
|
+
Copyright (C) 2013, 2014 CERN.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
8
|
+
a copy of this software and associated documentation files (the
|
|
9
|
+
"Software"), to deal in the Software without restriction, including
|
|
10
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
11
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
12
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
13
|
+
the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be
|
|
16
|
+
included in all copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
20
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
21
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
22
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
23
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
24
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
25
|
+
|
|
26
|
+
In applying this license, CERN does not waive the privileges and
|
|
27
|
+
immunities granted to it by virtue of its status as an
|
|
28
|
+
Intergovernmental Organization or submit itself to any jurisdiction.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pl-dict-diff
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Dictdiffer is a library that helps you to diff and patch dictionaries.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Invenio Collaboration
|
|
8
|
+
Author-email: info@inveniosoftware.org
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Provides-Extra: all
|
|
20
|
+
Provides-Extra: numpy
|
|
21
|
+
Provides-Extra: tests
|
|
22
|
+
Requires-Dist: numpy (>=2.4.6,<3) ; extra == "all"
|
|
23
|
+
Requires-Dist: numpy (>=2.4.6,<3) ; extra == "numpy"
|
|
24
|
+
Requires-Dist: pytest (>=9.1.1,<10) ; extra == "all"
|
|
25
|
+
Requires-Dist: pytest (>=9.1.1,<10) ; extra == "tests"
|
|
26
|
+
Project-URL: Homepage, https://github.com/inspirehep/dictdiffer
|
|
27
|
+
Project-URL: Repository, https://github.com/inspirehep/dictdiffer
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Dictdiffer
|
|
32
|
+
Dictdiffer is a helper module that helps you to diff and patch dictionaries.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
Requires Python 3.11 or newer.
|
|
36
|
+
|
|
37
|
+
Install using pip
|
|
38
|
+
```bash
|
|
39
|
+
pip install git+https://github.com/inspirehep/dictdiffer.git
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
Let's start with an example on how to find the diff between two dictionaries using :func:`.diff` method:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from inspire_dictdiffer import diff, patch, swap, revert
|
|
47
|
+
|
|
48
|
+
first = {
|
|
49
|
+
"title": "hello",
|
|
50
|
+
"fork_count": 20,
|
|
51
|
+
"stargazers": ["/users/20", "/users/30"],
|
|
52
|
+
"settings": {
|
|
53
|
+
"assignees": [100, 101, 201],
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
second = {
|
|
58
|
+
"title": "hellooo",
|
|
59
|
+
"fork_count": 20,
|
|
60
|
+
"stargazers": ["/users/20", "/users/30", "/users/40"],
|
|
61
|
+
"settings": {
|
|
62
|
+
"assignees": [100, 101, 202],
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
result = diff(first, second)
|
|
67
|
+
|
|
68
|
+
assert list(result) == [
|
|
69
|
+
('change', ['settings', 'assignees', 2], (201, 202)),
|
|
70
|
+
('add', 'stargazers', [(2, '/users/40')]),
|
|
71
|
+
('change', 'title', ('hello', 'hellooo'))]
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Now we can apply the diff result with `.patch()` method:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
result = diff(first, second)
|
|
79
|
+
patched = patch(result, first)
|
|
80
|
+
|
|
81
|
+
assert patched == second
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Also we can swap the diff result with `.swap()` method:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
result = diff(first, second)
|
|
88
|
+
swapped = swap(result)
|
|
89
|
+
|
|
90
|
+
assert list(swapped) == [
|
|
91
|
+
('change', ['settings', 'assignees', 2], (202, 201)),
|
|
92
|
+
('remove', 'stargazers', [(2, '/users/40')]),
|
|
93
|
+
('change', 'title', ('hellooo', 'hello'))]
|
|
94
|
+
```
|
|
95
|
+
Let's revert the last changes:
|
|
96
|
+
```python
|
|
97
|
+
result = diff(first, second)
|
|
98
|
+
reverted = revert(result, patched)
|
|
99
|
+
assert reverted == first
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A tolerance can be used to consider closed values as equal.
|
|
103
|
+
The tolerance parameter only applies for int and float.
|
|
104
|
+
Let's try with a tolerance of 10% with the values 10 and 10.5:
|
|
105
|
+
```python
|
|
106
|
+
first = {'a': 10.0}
|
|
107
|
+
second = {'a': 10.5}
|
|
108
|
+
result = diff(first, second, tolerance=0.1)
|
|
109
|
+
|
|
110
|
+
assert list(result) == []
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Now with a tolerance of 1%:
|
|
114
|
+
```python
|
|
115
|
+
result = diff(first, second, tolerance=0.01)
|
|
116
|
+
|
|
117
|
+
assert list(result) == ('change', 'a', (10.0, 10.5))
|
|
118
|
+
```
|
|
119
|
+
## Testing
|
|
120
|
+
Running the test suite is as simple as:
|
|
121
|
+
```bash
|
|
122
|
+
poetry install --with dev --extras numpy
|
|
123
|
+
poetry run pytest
|
|
124
|
+
```
|
|
125
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
|
|
2
|
+
# Dictdiffer
|
|
3
|
+
Dictdiffer is a helper module that helps you to diff and patch dictionaries.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
Requires Python 3.11 or newer.
|
|
7
|
+
|
|
8
|
+
Install using pip
|
|
9
|
+
```bash
|
|
10
|
+
pip install git+https://github.com/inspirehep/dictdiffer.git
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
Let's start with an example on how to find the diff between two dictionaries using :func:`.diff` method:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from inspire_dictdiffer import diff, patch, swap, revert
|
|
18
|
+
|
|
19
|
+
first = {
|
|
20
|
+
"title": "hello",
|
|
21
|
+
"fork_count": 20,
|
|
22
|
+
"stargazers": ["/users/20", "/users/30"],
|
|
23
|
+
"settings": {
|
|
24
|
+
"assignees": [100, 101, 201],
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
second = {
|
|
29
|
+
"title": "hellooo",
|
|
30
|
+
"fork_count": 20,
|
|
31
|
+
"stargazers": ["/users/20", "/users/30", "/users/40"],
|
|
32
|
+
"settings": {
|
|
33
|
+
"assignees": [100, 101, 202],
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
result = diff(first, second)
|
|
38
|
+
|
|
39
|
+
assert list(result) == [
|
|
40
|
+
('change', ['settings', 'assignees', 2], (201, 202)),
|
|
41
|
+
('add', 'stargazers', [(2, '/users/40')]),
|
|
42
|
+
('change', 'title', ('hello', 'hellooo'))]
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Now we can apply the diff result with `.patch()` method:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
result = diff(first, second)
|
|
50
|
+
patched = patch(result, first)
|
|
51
|
+
|
|
52
|
+
assert patched == second
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Also we can swap the diff result with `.swap()` method:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
result = diff(first, second)
|
|
59
|
+
swapped = swap(result)
|
|
60
|
+
|
|
61
|
+
assert list(swapped) == [
|
|
62
|
+
('change', ['settings', 'assignees', 2], (202, 201)),
|
|
63
|
+
('remove', 'stargazers', [(2, '/users/40')]),
|
|
64
|
+
('change', 'title', ('hellooo', 'hello'))]
|
|
65
|
+
```
|
|
66
|
+
Let's revert the last changes:
|
|
67
|
+
```python
|
|
68
|
+
result = diff(first, second)
|
|
69
|
+
reverted = revert(result, patched)
|
|
70
|
+
assert reverted == first
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A tolerance can be used to consider closed values as equal.
|
|
74
|
+
The tolerance parameter only applies for int and float.
|
|
75
|
+
Let's try with a tolerance of 10% with the values 10 and 10.5:
|
|
76
|
+
```python
|
|
77
|
+
first = {'a': 10.0}
|
|
78
|
+
second = {'a': 10.5}
|
|
79
|
+
result = diff(first, second, tolerance=0.1)
|
|
80
|
+
|
|
81
|
+
assert list(result) == []
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Now with a tolerance of 1%:
|
|
85
|
+
```python
|
|
86
|
+
result = diff(first, second, tolerance=0.01)
|
|
87
|
+
|
|
88
|
+
assert list(result) == ('change', 'a', (10.0, 10.5))
|
|
89
|
+
```
|
|
90
|
+
## Testing
|
|
91
|
+
Running the test suite is as simple as:
|
|
92
|
+
```bash
|
|
93
|
+
poetry install --with dev --extras numpy
|
|
94
|
+
poetry run pytest
|
|
95
|
+
```
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# This file is part of Dictdiffer.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2013 Fatih Erikli.
|
|
4
|
+
# Copyright (C) 2013, 2014, 2015, 2016 CERN.
|
|
5
|
+
# Copyright (C) 2017-2019 ETH Zurich, Swiss Data Science Center, Jiri Kuncar.
|
|
6
|
+
#
|
|
7
|
+
# Dictdiffer is free software; you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the MIT License; see LICENSE file for more
|
|
9
|
+
# details.
|
|
10
|
+
|
|
11
|
+
"""Dictdiffer is a helper module to diff and patch dictionaries."""
|
|
12
|
+
|
|
13
|
+
from collections.abc import Iterable, MutableMapping, MutableSequence, MutableSet
|
|
14
|
+
from copy import deepcopy
|
|
15
|
+
|
|
16
|
+
from .utils import (
|
|
17
|
+
EPSILON,
|
|
18
|
+
PathLimit,
|
|
19
|
+
RemovedObject,
|
|
20
|
+
are_different,
|
|
21
|
+
dot_lookup,
|
|
22
|
+
strip_removed_objects,
|
|
23
|
+
)
|
|
24
|
+
from .version import __version__
|
|
25
|
+
|
|
26
|
+
(ADD, REMOVE, CHANGE) = ("add", "remove", "change")
|
|
27
|
+
|
|
28
|
+
__all__ = (
|
|
29
|
+
"diff",
|
|
30
|
+
"patch",
|
|
31
|
+
"swap",
|
|
32
|
+
"revert",
|
|
33
|
+
"dot_lookup",
|
|
34
|
+
"__version__",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
DICT_TYPES = (MutableMapping,)
|
|
38
|
+
LIST_TYPES = (MutableSequence,)
|
|
39
|
+
SET_TYPES = (MutableSet,)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
import numpy
|
|
43
|
+
|
|
44
|
+
HAS_NUMPY = True
|
|
45
|
+
LIST_TYPES += (numpy.ndarray,)
|
|
46
|
+
except ImportError: # pragma: no cover
|
|
47
|
+
HAS_NUMPY = False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def diff(
|
|
51
|
+
first,
|
|
52
|
+
second,
|
|
53
|
+
node=None,
|
|
54
|
+
ignore=None,
|
|
55
|
+
path_limit=None,
|
|
56
|
+
expand=False,
|
|
57
|
+
tolerance=EPSILON,
|
|
58
|
+
absolute_tolerance=None,
|
|
59
|
+
dot_notation=True,
|
|
60
|
+
):
|
|
61
|
+
"""Compare two dictionary/list/set objects, and returns a diff result.
|
|
62
|
+
|
|
63
|
+
Return an iterator with differences between two objects. The diff items
|
|
64
|
+
represent addition/deletion/change and the item value is a *deep copy*
|
|
65
|
+
from the corresponding source or destination objects.
|
|
66
|
+
|
|
67
|
+
>>> from dictdiffer import diff
|
|
68
|
+
>>> result = diff({'a': 'b'}, {'a': 'c'})
|
|
69
|
+
>>> list(result)
|
|
70
|
+
[('change', 'a', ('b', 'c'))]
|
|
71
|
+
|
|
72
|
+
The keys can be skipped from difference calculation when they are included
|
|
73
|
+
in ``ignore`` argument of type :class:`collections.Container`.
|
|
74
|
+
|
|
75
|
+
>>> list(diff({'a': 1, 'b': 2}, {'a': 3, 'b': 4}, ignore=set(['a'])))
|
|
76
|
+
[('change', 'b', (2, 4))]
|
|
77
|
+
>>> class IgnoreCase(set):
|
|
78
|
+
... def __contains__(self, key):
|
|
79
|
+
... return set.__contains__(self, str(key).lower())
|
|
80
|
+
>>> list(diff({'a': 1, 'b': 2}, {'A': 3, 'b': 4}, ignore=IgnoreCase('a')))
|
|
81
|
+
[('change', 'b', (2, 4))]
|
|
82
|
+
|
|
83
|
+
The difference calculation can be limitted to certain path:
|
|
84
|
+
|
|
85
|
+
>>> list(diff({}, {'a': {'b': 'c'}}))
|
|
86
|
+
[('add', '', [('a', {'b': 'c'})])]
|
|
87
|
+
|
|
88
|
+
>>> from dictdiffer.utils import PathLimit
|
|
89
|
+
>>> list(diff({}, {'a': {'b': 'c'}}, path_limit=PathLimit()))
|
|
90
|
+
[('add', '', [('a', {})]), ('add', 'a', [('b', 'c')])]
|
|
91
|
+
|
|
92
|
+
>>> from dictdiffer.utils import PathLimit
|
|
93
|
+
>>> list(diff({}, {'a': {'b': 'c'}}, path_limit=PathLimit([('a',)])))
|
|
94
|
+
[('add', '', [('a', {'b': 'c'})])]
|
|
95
|
+
|
|
96
|
+
>>> from dictdiffer.utils import PathLimit
|
|
97
|
+
>>> list(diff({}, {'a': {'b': 'c'}},
|
|
98
|
+
... path_limit=PathLimit([('a', 'b')])))
|
|
99
|
+
[('add', '', [('a', {})]), ('add', 'a', [('b', 'c')])]
|
|
100
|
+
|
|
101
|
+
>>> from dictdiffer.utils import PathLimit
|
|
102
|
+
>>> list(diff({'a': {'b': 'c'}}, {'a': {'b': 'c'}}, path_limit=PathLimit([('a',)])))
|
|
103
|
+
[]
|
|
104
|
+
|
|
105
|
+
The patch can be expanded to small units e.g. when adding multiple values:
|
|
106
|
+
|
|
107
|
+
>>> list(diff({'fruits': []}, {'fruits': ['apple', 'mango']}))
|
|
108
|
+
[('add', 'fruits', [(0, 'apple'), (1, 'mango')])]
|
|
109
|
+
|
|
110
|
+
>>> list(diff({'fruits': []}, {'fruits': ['apple', 'mango']}, expand=True))
|
|
111
|
+
[('add', 'fruits', [(0, 'apple')]), ('add', 'fruits', [(1, 'mango')])]
|
|
112
|
+
|
|
113
|
+
>>> list(diff({'a': {'x': 1}}, {'a': {'x': 2}}))
|
|
114
|
+
[('change', 'a.x', (1, 2))]
|
|
115
|
+
|
|
116
|
+
>>> list(diff({'a': {'x': 1}}, {'a': {'x': 2}},
|
|
117
|
+
... dot_notation=False))
|
|
118
|
+
[('change', ['a', 'x'], (1, 2))]
|
|
119
|
+
|
|
120
|
+
:param first: The original dictionary, ``list`` or ``set``.
|
|
121
|
+
:param second: New dictionary, ``list`` or ``set``.
|
|
122
|
+
:param node: Key for comparison that can be used in :func:`dot_lookup`.
|
|
123
|
+
:param ignore: Set of keys that should not be checked.
|
|
124
|
+
:param path_limit: List of path limit tuples or dictdiffer.utils.Pathlimit
|
|
125
|
+
object to limit the diff recursion depth.
|
|
126
|
+
A diff is still performed beyond the path_limit,
|
|
127
|
+
but individual differences will be aggregated up to the path_limit.
|
|
128
|
+
:param expand: Expand the patches.
|
|
129
|
+
:param tolerance: Threshold to consider when comparing two float numbers.
|
|
130
|
+
:param absolute_tolerance: Absolute threshold to consider when comparing
|
|
131
|
+
two float numbers.
|
|
132
|
+
:param dot_notation: Boolean to toggle dot notation on and off.
|
|
133
|
+
|
|
134
|
+
.. versionchanged:: 0.3
|
|
135
|
+
Added *ignore* parameter.
|
|
136
|
+
|
|
137
|
+
.. versionchanged:: 0.4
|
|
138
|
+
Arguments ``first`` and ``second`` can now contain a ``set``.
|
|
139
|
+
|
|
140
|
+
.. versionchanged:: 0.5
|
|
141
|
+
Added *path_limit* parameter.
|
|
142
|
+
Added *expand* paramter.
|
|
143
|
+
Added *tolerance* parameter.
|
|
144
|
+
|
|
145
|
+
.. versionchanged:: 0.7
|
|
146
|
+
Diff items are deep copies from its corresponding objects.
|
|
147
|
+
Argument *ignore* is always converted to a ``set``.
|
|
148
|
+
|
|
149
|
+
.. versionchanged:: 0.8
|
|
150
|
+
Added *dot_notation* parameter.
|
|
151
|
+
"""
|
|
152
|
+
if path_limit is not None and not isinstance(path_limit, PathLimit):
|
|
153
|
+
path_limit = PathLimit(path_limit)
|
|
154
|
+
|
|
155
|
+
if isinstance(ignore, Iterable):
|
|
156
|
+
|
|
157
|
+
def _process_ignore_value(value):
|
|
158
|
+
if isinstance(value, int):
|
|
159
|
+
return (value,)
|
|
160
|
+
elif isinstance(value, list):
|
|
161
|
+
return tuple(value)
|
|
162
|
+
elif not dot_notation and isinstance(value, str):
|
|
163
|
+
return (value,)
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
ignore = type(ignore)(_process_ignore_value(value) for value in ignore)
|
|
167
|
+
|
|
168
|
+
def dotted(node, default_type=list):
|
|
169
|
+
"""Return dotted notation."""
|
|
170
|
+
if dot_notation and all(
|
|
171
|
+
map(lambda x: isinstance(x, str) and "." not in x, node)
|
|
172
|
+
):
|
|
173
|
+
return ".".join(node)
|
|
174
|
+
else:
|
|
175
|
+
return default_type(node)
|
|
176
|
+
|
|
177
|
+
def _diff_recursive(_first, _second, _node=None):
|
|
178
|
+
_node = _node or []
|
|
179
|
+
|
|
180
|
+
dotted_node = dotted(_node)
|
|
181
|
+
|
|
182
|
+
differ = False
|
|
183
|
+
|
|
184
|
+
if isinstance(_first, DICT_TYPES) and isinstance(_second, DICT_TYPES):
|
|
185
|
+
# dictionaries are not hashable, we can't use sets
|
|
186
|
+
def check(key):
|
|
187
|
+
"""Test if key in current node should be ignored."""
|
|
188
|
+
return ignore is None or (
|
|
189
|
+
dotted(_node + [key], default_type=tuple) not in ignore
|
|
190
|
+
and tuple(_node + [key]) not in ignore
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
intersection = [k for k in _first if k in _second and check(k)]
|
|
194
|
+
addition = [k for k in _second if k not in _first and check(k)]
|
|
195
|
+
deletion = [k for k in _first if k not in _second and check(k)]
|
|
196
|
+
|
|
197
|
+
differ = True
|
|
198
|
+
|
|
199
|
+
elif isinstance(_first, LIST_TYPES) and isinstance(_second, LIST_TYPES):
|
|
200
|
+
len_first = len(_first)
|
|
201
|
+
len_second = len(_second)
|
|
202
|
+
|
|
203
|
+
intersection = list(range(0, min(len_first, len_second)))
|
|
204
|
+
addition = list(range(min(len_first, len_second), len_second))
|
|
205
|
+
deletion = list(reversed(range(min(len_first, len_second), len_first)))
|
|
206
|
+
|
|
207
|
+
differ = True
|
|
208
|
+
|
|
209
|
+
elif isinstance(_first, SET_TYPES) and isinstance(_second, SET_TYPES):
|
|
210
|
+
# Deep copy is not necessary for hashable items.
|
|
211
|
+
addition = _second - _first
|
|
212
|
+
if len(addition):
|
|
213
|
+
yield ADD, dotted_node, [(0, addition)]
|
|
214
|
+
deletion = _first - _second
|
|
215
|
+
if len(deletion):
|
|
216
|
+
yield REMOVE, dotted_node, [(0, deletion)]
|
|
217
|
+
|
|
218
|
+
return # stop here for sets
|
|
219
|
+
|
|
220
|
+
if differ:
|
|
221
|
+
# Compare if object is a dictionary or list.
|
|
222
|
+
#
|
|
223
|
+
# NOTE variables: intersection, addition, deletion contain only
|
|
224
|
+
# hashable types, hence they do not need to be deepcopied.
|
|
225
|
+
#
|
|
226
|
+
# Call again the parent function as recursive if dictionary have
|
|
227
|
+
# child objects. Yields `add` and `remove` flags.
|
|
228
|
+
for key in intersection:
|
|
229
|
+
# if type is not changed,
|
|
230
|
+
# callees again diff function to compare.
|
|
231
|
+
# otherwise, the change will be handled as `change` flag.
|
|
232
|
+
if path_limit and path_limit.path_is_limit(_node + [key]):
|
|
233
|
+
if _first[key] == _second[key]:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
yield (
|
|
237
|
+
CHANGE,
|
|
238
|
+
_node + [key],
|
|
239
|
+
(deepcopy(_first[key]), deepcopy(_second[key])),
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
recurred = _diff_recursive(
|
|
243
|
+
_first[key],
|
|
244
|
+
_second[key],
|
|
245
|
+
_node=_node + [key],
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
for diffed in recurred:
|
|
249
|
+
yield diffed
|
|
250
|
+
|
|
251
|
+
if addition:
|
|
252
|
+
if path_limit:
|
|
253
|
+
collect = []
|
|
254
|
+
collect_recurred = []
|
|
255
|
+
for key in addition:
|
|
256
|
+
if not isinstance(
|
|
257
|
+
_second[key], SET_TYPES + LIST_TYPES + DICT_TYPES
|
|
258
|
+
):
|
|
259
|
+
collect.append((key, deepcopy(_second[key])))
|
|
260
|
+
elif path_limit.path_is_limit(_node + [key]):
|
|
261
|
+
collect.append((key, deepcopy(_second[key])))
|
|
262
|
+
else:
|
|
263
|
+
collect.append((key, _second[key].__class__()))
|
|
264
|
+
recurred = _diff_recursive(
|
|
265
|
+
_second[key].__class__(),
|
|
266
|
+
_second[key],
|
|
267
|
+
_node=_node + [key],
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
collect_recurred.append(recurred)
|
|
271
|
+
|
|
272
|
+
if expand:
|
|
273
|
+
for key, val in collect:
|
|
274
|
+
yield ADD, dotted_node, [(key, val)]
|
|
275
|
+
else:
|
|
276
|
+
yield ADD, dotted_node, collect
|
|
277
|
+
|
|
278
|
+
for recurred in collect_recurred:
|
|
279
|
+
for diffed in recurred:
|
|
280
|
+
yield diffed
|
|
281
|
+
else:
|
|
282
|
+
if expand:
|
|
283
|
+
for key in addition:
|
|
284
|
+
yield ADD, dotted_node, [(key, deepcopy(_second[key]))]
|
|
285
|
+
else:
|
|
286
|
+
yield (
|
|
287
|
+
ADD,
|
|
288
|
+
dotted_node,
|
|
289
|
+
[
|
|
290
|
+
# for additions, return a list that consist with
|
|
291
|
+
# two-pair tuples.
|
|
292
|
+
(key, deepcopy(_second[key]))
|
|
293
|
+
for key in addition
|
|
294
|
+
],
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if deletion:
|
|
298
|
+
if expand:
|
|
299
|
+
for key in deletion:
|
|
300
|
+
yield REMOVE, dotted_node, [(key, deepcopy(_first[key]))]
|
|
301
|
+
else:
|
|
302
|
+
yield (
|
|
303
|
+
REMOVE,
|
|
304
|
+
dotted_node,
|
|
305
|
+
[
|
|
306
|
+
# for deletions, return the list of removed keys
|
|
307
|
+
# and values.
|
|
308
|
+
(key, deepcopy(_first[key]))
|
|
309
|
+
for key in deletion
|
|
310
|
+
],
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
else:
|
|
314
|
+
# Compare string and numerical types and yield `change` flag.
|
|
315
|
+
if are_different(_first, _second, tolerance, absolute_tolerance):
|
|
316
|
+
yield CHANGE, dotted_node, (deepcopy(_first), deepcopy(_second))
|
|
317
|
+
|
|
318
|
+
return _diff_recursive(first, second, node)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def patch(diff_result, destination, in_place=False):
|
|
322
|
+
"""Patch the diff result to the destination dictionary.
|
|
323
|
+
|
|
324
|
+
:param diff_result: Changes returned by ``diff``.
|
|
325
|
+
:param destination: Structure to apply the changes to.
|
|
326
|
+
:param in_place: By default, destination dictionary is deep copied
|
|
327
|
+
before applying the patch, and the copy is returned.
|
|
328
|
+
Setting ``in_place=True`` means that patch will apply
|
|
329
|
+
the changes directly to and return the destination
|
|
330
|
+
structure.
|
|
331
|
+
"""
|
|
332
|
+
if not in_place:
|
|
333
|
+
destination = deepcopy(destination)
|
|
334
|
+
|
|
335
|
+
def add(node, changes):
|
|
336
|
+
for key, value in changes:
|
|
337
|
+
dest = dot_lookup(destination, node)
|
|
338
|
+
if isinstance(dest, LIST_TYPES):
|
|
339
|
+
dest.insert(key, value)
|
|
340
|
+
elif isinstance(dest, SET_TYPES):
|
|
341
|
+
dest |= value
|
|
342
|
+
else:
|
|
343
|
+
dest[key] = value
|
|
344
|
+
|
|
345
|
+
def change(node, changes):
|
|
346
|
+
dest = dot_lookup(destination, node, parent=True)
|
|
347
|
+
if isinstance(node, str):
|
|
348
|
+
last_node = node.split(".")[-1]
|
|
349
|
+
else:
|
|
350
|
+
last_node = node[-1]
|
|
351
|
+
if isinstance(dest, LIST_TYPES):
|
|
352
|
+
last_node = int(last_node)
|
|
353
|
+
_, value = changes
|
|
354
|
+
dest[last_node] = value
|
|
355
|
+
|
|
356
|
+
def remove(node, changes):
|
|
357
|
+
for key, value in changes:
|
|
358
|
+
dest = dot_lookup(destination, node)
|
|
359
|
+
if isinstance(dest, SET_TYPES):
|
|
360
|
+
dest -= value
|
|
361
|
+
elif isinstance(dest, LIST_TYPES):
|
|
362
|
+
dest[key] = RemovedObject()
|
|
363
|
+
else:
|
|
364
|
+
del dest[key]
|
|
365
|
+
|
|
366
|
+
patchers = {REMOVE: remove, ADD: add, CHANGE: change}
|
|
367
|
+
|
|
368
|
+
for action, node, changes in diff_result:
|
|
369
|
+
patchers[action](node, changes)
|
|
370
|
+
|
|
371
|
+
return strip_removed_objects(destination)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def swap(diff_result):
|
|
375
|
+
"""Swap the diff result.
|
|
376
|
+
|
|
377
|
+
It uses following mapping:
|
|
378
|
+
|
|
379
|
+
- remove -> add
|
|
380
|
+
- add -> remove
|
|
381
|
+
|
|
382
|
+
In addition, swap the changed values for `change` flag.
|
|
383
|
+
|
|
384
|
+
>>> from dictdiffer import swap
|
|
385
|
+
>>> swapped = swap([('add', 'a.b.c', [('a', 'b'), ('c', 'd')])])
|
|
386
|
+
>>> next(swapped)
|
|
387
|
+
('remove', 'a.b.c', [('c', 'd'), ('a', 'b')])
|
|
388
|
+
|
|
389
|
+
>>> swapped = swap([('change', 'a.b.c', ('a', 'b'))])
|
|
390
|
+
>>> next(swapped)
|
|
391
|
+
('change', 'a.b.c', ('b', 'a'))
|
|
392
|
+
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
def add(node, changes):
|
|
396
|
+
return REMOVE, node, list(reversed(changes))
|
|
397
|
+
|
|
398
|
+
def remove(node, changes):
|
|
399
|
+
return ADD, node, changes
|
|
400
|
+
|
|
401
|
+
def change(node, changes):
|
|
402
|
+
first, second = changes
|
|
403
|
+
return CHANGE, node, (second, first)
|
|
404
|
+
|
|
405
|
+
swappers = {REMOVE: remove, ADD: add, CHANGE: change}
|
|
406
|
+
|
|
407
|
+
for action, node, change in diff_result:
|
|
408
|
+
yield swappers[action](node, change)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def revert(diff_result, destination, in_place=False):
|
|
412
|
+
"""Call swap function to revert patched dictionary object.
|
|
413
|
+
|
|
414
|
+
Usage example:
|
|
415
|
+
|
|
416
|
+
>>> from dictdiffer import diff, revert
|
|
417
|
+
>>> first = {'a': 'b'}
|
|
418
|
+
>>> second = {'a': 'c'}
|
|
419
|
+
>>> revert(diff(first, second), second)
|
|
420
|
+
{'a': 'b'}
|
|
421
|
+
|
|
422
|
+
:param diff_result: Changes returned by ``diff``.
|
|
423
|
+
:param destination: Structure to apply the changes to.
|
|
424
|
+
:param in_place: By default, destination dictionary is deep
|
|
425
|
+
copied before being reverted, and the copy
|
|
426
|
+
is returned. Setting ``in_place=True`` means
|
|
427
|
+
that revert will apply the changes directly to
|
|
428
|
+
and return the destination structure.
|
|
429
|
+
"""
|
|
430
|
+
return patch(swap(diff_result), destination, in_place)
|