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.
@@ -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)