pl-dict-diff 0.0.1__py3-none-any.whl

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,153 @@
1
+ # This file is part of Dictdiffer.
2
+ #
3
+ # Copyright (C) 2015 CERN.
4
+ # Copyright (C) 2017 ETH Zurich, Swiss Data Science Center, Jiri Kuncar.
5
+ #
6
+ # Dictdiffer is free software; you can redistribute it and/or modify
7
+ # it under the terms of the MIT License; see LICENSE file for more
8
+ # details.
9
+
10
+ """Sub module to handle the conflict resolution."""
11
+
12
+ from .utils import get_path
13
+
14
+
15
+ class UnresolvedConflictsException(Exception):
16
+ """Exception raised in case of an unresolveable conflict.
17
+
18
+ Exception raised in case of conflicts, that can not be resolved using
19
+ the provided actions in the automated merging process.
20
+ """
21
+
22
+ def __init__(self, unresolved_conflicts):
23
+ """Initialize the UnresolvedConflictsException.
24
+
25
+ :param unresolved_conflicts: list of unresolved conflicts.
26
+ dictdiffer.conflict.Conflict objects.
27
+ """
28
+ self.message = (
29
+ "The unresolved conflicts are stored in the *content* "
30
+ "attribute of this exception or in the "
31
+ "*unresolved_conflicts* attribute of the "
32
+ "dictdiffer.merge.Merger object."
33
+ )
34
+ self.content = unresolved_conflicts
35
+
36
+ def __repr__(self):
37
+ """Return the object representation."""
38
+ return self.message
39
+
40
+ def __str__(self):
41
+ """Return the string representation."""
42
+ return self.message
43
+
44
+
45
+ class NoFurtherResolutionException(Exception):
46
+ """Exception raised to stop the automated resolution process.
47
+
48
+ Raised in case that the automatic conflict resolution process should stop
49
+ trying more general keys.
50
+ """
51
+
52
+ pass
53
+
54
+
55
+ class Resolver:
56
+ """Class handling the conflict resolution process.
57
+
58
+ Presents the given conflicts to actions designed to solve them.
59
+ """
60
+
61
+ def __init__(self, actions, additional_info=None):
62
+ """Initialize the Resolver.
63
+
64
+ :param action: dict object containing the necessary resolution
65
+ functions
66
+ :param additional_info: any additional information required by the
67
+ actions
68
+ """
69
+ self.actions = actions
70
+ self.additional_info = additional_info
71
+
72
+ self.unresolved_conflicts = []
73
+
74
+ def _auto_resolve(self, conflict):
75
+ """Try to auto resolve conflicts.
76
+
77
+ Method trying to auto resolve conflicts in case that the perform the
78
+ same amendment.
79
+ """
80
+ if conflict.first_patch == conflict.second_patch:
81
+ conflict.take = "f"
82
+ return True
83
+ return False
84
+
85
+ def _find_conflicting_path(self, conflict):
86
+ """Return the shortest path commown to two patches."""
87
+ p1p = get_path(conflict.first_patch)
88
+ p2p = get_path(conflict.second_patch)
89
+
90
+ # This returns the shortest path
91
+ return p1p if len(p1p) <= len(p2p) else p2p
92
+
93
+ def _consecutive_slices(self, iterable):
94
+ """Build a list of consecutive slices of a given path.
95
+
96
+ >>> r = Resolver(None, None)
97
+ >>> list(r._consecutive_slices([1, 2, 3]))
98
+ [[1, 2, 3], [1, 2], [1]]
99
+ """
100
+ return (iterable[:i] for i in reversed(range(1, len(iterable) + 1)))
101
+
102
+ def resolve_conflicts(self, first_patches, second_patches, conflicts):
103
+ """Convert the given conflicts to the actions.
104
+
105
+ The method, will map the conflicts to an actions based on the path of
106
+ the conflict. In case that the resolution attempt is not successful, it
107
+ will strip the last element of the path and try again, until the
108
+ resolution is just not possible.
109
+
110
+ :param first_patches: list of dictdiffer.diff patches
111
+ :param second_patches: list of dictdiffer.diff patches
112
+ :param conflicts: list of Conflict objects
113
+ """
114
+ for conflict in conflicts:
115
+ conflict_path = self._find_conflicting_path(conflict)
116
+
117
+ if self._auto_resolve(conflict):
118
+ continue
119
+ # Let's do some cascading here
120
+ for sub_path in self._consecutive_slices(conflict_path):
121
+ try:
122
+ if self.actions[sub_path](
123
+ conflict, first_patches, second_patches, self.additional_info
124
+ ):
125
+ break
126
+ except NoFurtherResolutionException:
127
+ self.unresolved_conflicts.append(conflict)
128
+ break
129
+ except KeyError:
130
+ pass
131
+ else:
132
+ # The conflict could not be resolved
133
+ self.unresolved_conflicts.append(conflict)
134
+
135
+ if self.unresolved_conflicts:
136
+ raise UnresolvedConflictsException(self.unresolved_conflicts)
137
+
138
+ def manual_resolve_conflicts(self, picks):
139
+ """Resolve manually the conflicts.
140
+
141
+ This method resolves conflicts that could not be resolved in an
142
+ automatic way. The picks parameter utilized the *take* attribute of the
143
+ Conflict objects.
144
+
145
+ :param picks: list of 'f' or 's' strings, utilizing the *take*
146
+ parameter of each Conflict object
147
+ """
148
+ if len(picks) != len(self.unresolved_conflicts):
149
+ raise UnresolvedConflictsException(self.unresolved_conflicts)
150
+ for pick, conflict in zip(picks, self.unresolved_conflicts):
151
+ conflict.take = pick
152
+
153
+ self.unresolved_conflicts = []
@@ -0,0 +1,36 @@
1
+ # This file is part of Dictdiffer.
2
+ #
3
+ # Copyright (C) 2021 CERN.
4
+ #
5
+ # Dictdiffer is free software; you can redistribute it and/or modify
6
+ # it under the terms of the MIT License; see LICENSE file for more
7
+ # details.
8
+ """Define helper functions for testing."""
9
+
10
+ from pprint import pformat
11
+
12
+ from . import diff
13
+
14
+
15
+ def assert_no_diff(*args, **kwargs):
16
+ """Compare two dictionary/list/set objects and raise error on difference.
17
+
18
+ When there is a difference, this will print a formatted diff.
19
+ This is especially useful for pytest.
20
+
21
+ Usage example:
22
+
23
+ >>> from dictdiffer.testing import assert_no_diff
24
+ >>> result = {'a': 'b'}
25
+ >>> expected = {'a': 'c'}
26
+ >>> assert_no_diff(result, expected)
27
+ Traceback (most recent call last):
28
+ File "<stdin>", line 1, in <module>
29
+ File "<stdin>", line 14, in assert_no_diff
30
+ AssertionError: [('change', 'a', ('b', 'c'))]
31
+
32
+ :param args: Positional arguments to the ``diff`` function.
33
+ :param second: Named arguments to the ``diff`` function.
34
+ """
35
+ d = [d for d in diff(*args, **kwargs)]
36
+ assert not d, pformat(d)
@@ -0,0 +1,53 @@
1
+ # This file is part of Dictdiffer.
2
+ #
3
+ # Copyright (C) 2015 CERN.
4
+ #
5
+ # Dictdiffer is free software; you can redistribute it and/or modify
6
+ # it under the terms of the MIT License; see LICENSE file for more
7
+ # details.
8
+
9
+ """Sub module to handle the unification of patches after the merge."""
10
+
11
+ from .utils import get_path, nested_hash
12
+
13
+
14
+ class Unifier:
15
+ """Class handling the unification process after the merge."""
16
+
17
+ def unify(self, first_patches, second_patches, conflicts):
18
+ """Unify two lists of patches into one.
19
+
20
+ Takes into account their appearance in the given list of conflicts.
21
+
22
+ :param first_patches: list of dictdiffer.diff patches
23
+ :param second_patches: list of dictdiffer.diff patches
24
+ :param conflicts: list of Conflict objects
25
+ """
26
+ self.unified_patches = []
27
+ self._build_index(conflicts)
28
+
29
+ sorted_patches = sorted(first_patches + second_patches, key=get_path)
30
+
31
+ for patch in sorted_patches:
32
+ conflict = self._index.get(nested_hash(patch))
33
+
34
+ # Apply only the patches that were taken as part of conflict
35
+ # resolution.
36
+ if conflict:
37
+ if conflict.take_patch() != patch:
38
+ continue
39
+
40
+ self.unified_patches.append(patch)
41
+
42
+ return self.unified_patches
43
+
44
+ def _build_index(self, conflicts):
45
+ """Create a dictionary attribute mapping patches to conflicts.
46
+
47
+ Creates a dictionary attribute mapping the tuplefied version of each
48
+ patch to it's containing Conflict object.
49
+ """
50
+ self._index = {}
51
+ for conflict in conflicts:
52
+ self._index[nested_hash(conflict.first_patch)] = conflict
53
+ self._index[nested_hash(conflict.second_patch)] = conflict
@@ -0,0 +1,310 @@
1
+ # This file is part of Dictdiffer.
2
+ #
3
+ # Copyright (C) 2015 CERN.
4
+ # Copyright (C) 2017, 2019 ETH Zurich, Swiss Data Science Center, Jiri Kuncar.
5
+ #
6
+ # Dictdiffer is free software; you can redistribute it and/or modify
7
+ # it under the terms of the MIT License; see LICENSE file for more
8
+ # details.
9
+
10
+ """Utils gathers helper functions, classes for the dictdiffer module."""
11
+
12
+ import sys
13
+
14
+ from collections.abc import MutableMapping, MutableSequence
15
+ from itertools import zip_longest
16
+
17
+ EPSILON = sys.float_info.epsilon
18
+
19
+ DICT_TYPES = (MutableMapping,)
20
+ LIST_TYPES = (MutableSequence,)
21
+
22
+
23
+ class RemovedObject:
24
+ """Removed object placeholder."""
25
+
26
+
27
+ class WildcardDict(dict):
28
+ """Provide possibility to use special wildcard keys to access values.
29
+
30
+ Those wildcards are:
31
+ *: wildcard for everything that follows
32
+ +: wildcard for anything on the same path level
33
+ The intended use case of this are dictionaries, that utilize tuples as
34
+ keys.
35
+
36
+ >>> from dictdiffer.utils import WildcardDict
37
+ >>> w = WildcardDict({('foo', '*'): '* card',
38
+ ... ('banana', '+'): '+ card'})
39
+ >>> w[ ('foo', 'bar', 'baz') ]
40
+ '* card'
41
+ >>> w[ ('banana', 'apple') ]
42
+ '+ card'
43
+ """
44
+
45
+ def __init__(self, values=None):
46
+ """Set lookup key indices.
47
+
48
+ :param values: a dictionary
49
+ """
50
+ super().__init__()
51
+ self.star_keys = set()
52
+ self.plus_keys = set()
53
+
54
+ if values is not None:
55
+ for key, value in values.items():
56
+ self.__setitem__(key, value)
57
+
58
+ def __getitem__(self, key):
59
+ """Return the value corresponding to the key, regarding wildcards.
60
+
61
+ If the key doesn't exit it tries the '+' wildcard and then the
62
+ '*' wildcard.
63
+
64
+ >>> w = WildcardDict({('foo', '*'): '* card',
65
+ ... ('banana', '+'): '+ card'})
66
+ >>> w[ ('foo', 'bar') ]
67
+ '* card'
68
+ >>> w[ ('foo', 'bar', 'baz') ]
69
+ '* card'
70
+ >>> w[ ('banana', 'apple') ]
71
+ '+ card'
72
+ >>> w[ ('banana', 'apple', 'mango') ]
73
+ Traceback (most recent call last):
74
+ ...
75
+ KeyError
76
+ """
77
+ try:
78
+ return super().__getitem__(key)
79
+ except KeyError:
80
+ if key[:-1] in self.plus_keys:
81
+ return super().__getitem__(key[:-1] + ("+",))
82
+ for _key in [key[:-i] for i in range(1, len(key) + 1)]:
83
+ if _key in self.star_keys:
84
+ return super().__getitem__(_key + ("*",))
85
+ raise KeyError
86
+
87
+ def __setitem__(self, key, value):
88
+ """Set the item for a given key (path)."""
89
+ super().__setitem__(key, value)
90
+
91
+ if key[-1] == "+":
92
+ self.plus_keys.add(key[:-1])
93
+ if key[-1] == "*":
94
+ self.star_keys.add(key[:-1])
95
+
96
+ def query_path(self, key):
97
+ """Return the key (path) that matches the queried key.
98
+
99
+ >>> w = WildcardDict({('foo', '*'): 'banana'})
100
+ >>> w.query_path(('foo', 'bar', 'baz'))
101
+ ('foo', '*')
102
+ """
103
+ if key in self:
104
+ return key
105
+ if key[:-1] in self.plus_keys:
106
+ return key[:-1] + ("+",)
107
+ for _key in [key[:-i] for i in range(1, len(key) + 1)]:
108
+ if _key in self.star_keys:
109
+ return _key + ("*",)
110
+
111
+ raise KeyError
112
+
113
+
114
+ class PathLimit:
115
+ """Class to limit recursion depth during the dictdiffer.diff execution."""
116
+
117
+ def __init__(self, path_limits=[], final_key=None):
118
+ """Initialize a dictionary structure to determine a path limit.
119
+
120
+ :param path_limits: list of keys (tuples) determining the path limits
121
+ :param final_key: the key used in the dictionary to determin if the
122
+ path is final
123
+
124
+ >>> pl = PathLimit( [('foo', 'bar')] , final_key='!@#$%FINAL')
125
+ >>> pl.dict
126
+ {'foo': {'bar': {'!@#$%FINAL': True}}}
127
+ """
128
+ self.final_key = final_key if final_key else "!@#$FINAL"
129
+ self.dict = {}
130
+ for key_path in path_limits:
131
+ containing = self.dict
132
+ for key in key_path:
133
+ try:
134
+ containing = containing[key]
135
+ except KeyError:
136
+ containing[key] = {}
137
+ containing = containing[key]
138
+
139
+ containing[self.final_key] = True
140
+
141
+ def path_is_limit(self, key_path):
142
+ """Query the PathLimit object if the given key_path is a limit.
143
+
144
+ >>> pl = PathLimit( [('foo', 'bar')] , final_key='!@#$%FINAL')
145
+ >>> pl.path_is_limit( ('foo', 'bar') )
146
+ True
147
+ """
148
+ containing = self.dict
149
+ for key in key_path:
150
+ try:
151
+ containing = containing[key]
152
+ except KeyError:
153
+ try:
154
+ containing = containing["*"]
155
+ except KeyError:
156
+ return False
157
+
158
+ return containing.get(self.final_key, False)
159
+
160
+
161
+ def create_dotted_node(node):
162
+ """Create the *dotted node* notation for the dictdiffer.diff patches.
163
+
164
+ >>> create_dotted_node( ['foo', 'bar', 'baz'] )
165
+ 'foo.bar.baz'
166
+ """
167
+ if all(map(lambda x: isinstance(x, str), node)):
168
+ return ".".join(node)
169
+ else:
170
+ return list(node)
171
+
172
+
173
+ def get_path(patch):
174
+ """Return the path for a given dictdiffer.diff patch."""
175
+ if patch[1] != "":
176
+ keys = patch[1].split(".") if isinstance(patch[1], str) else patch[1]
177
+ else:
178
+ keys = []
179
+ keys = keys + [patch[2][0][0]] if patch[0] != "change" else keys
180
+ return tuple(keys)
181
+
182
+
183
+ def is_super_path(path1, path2):
184
+ """Check if one path is the super path of the other.
185
+
186
+ Super path means, that the n values in tuple are equal to the first n of m
187
+ vales in tuple b.
188
+
189
+ >>> is_super_path( ('foo', 'bar'), ('foo', 'bar') )
190
+ True
191
+
192
+ >>> is_super_path( ('foo', 'bar'), ('foo', 'bar', 'baz') )
193
+ True
194
+
195
+ >>> is_super_path( ('foo', 'bar'), ('foo', 'apple', 'banana') )
196
+ False
197
+ """
198
+ return all(map(lambda x: x[0] == x[1] or x[0] is None, zip_longest(path1, path2)))
199
+
200
+
201
+ def nested_hash(obj):
202
+ """Create a hash of nested, mutable data structures.
203
+
204
+ It shall be noted, that the uniqeness of those hashes in general cases is
205
+ not assured but it should be enough for the cases occurring during the
206
+ merging process.
207
+ """
208
+ try:
209
+ return hash(obj)
210
+ except TypeError:
211
+ if isinstance(obj, (list, tuple)):
212
+ return hash(tuple(map(nested_hash, obj)))
213
+ elif isinstance(obj, set):
214
+ return hash(tuple(map(nested_hash, sorted(obj))))
215
+ elif isinstance(obj, dict):
216
+ return hash(tuple(map(nested_hash, sorted(obj.items()))))
217
+
218
+
219
+ def dot_lookup(source, lookup, parent=False):
220
+ """Allow you to reach dictionary items with string or list lookup.
221
+
222
+ Recursively find value by lookup key split by '.'.
223
+
224
+ >>> from dictdiffer.utils import dot_lookup
225
+ >>> dot_lookup({'a': {'b': 'hello'}}, 'a.b')
226
+ 'hello'
227
+
228
+ If parent argument is True, returns the parent node of matched
229
+ object.
230
+
231
+ >>> dot_lookup({'a': {'b': 'hello'}}, 'a.b', parent=True)
232
+ {'b': 'hello'}
233
+
234
+ If node is empty value, returns the whole dictionary object.
235
+
236
+ >>> dot_lookup({'a': {'b': 'hello'}}, '')
237
+ {'a': {'b': 'hello'}}
238
+
239
+ """
240
+ if lookup is None or lookup == "" or lookup == []:
241
+ return source
242
+
243
+ value = source
244
+ if isinstance(lookup, str):
245
+ keys = lookup.split(".")
246
+ elif isinstance(lookup, list):
247
+ keys = lookup
248
+ else:
249
+ raise TypeError("lookup must be string or list")
250
+
251
+ if parent:
252
+ keys = keys[:-1]
253
+
254
+ for key in keys:
255
+ if isinstance(value, list):
256
+ key = int(key)
257
+ value = value[key]
258
+ return value
259
+
260
+
261
+ def are_different(first, second, tolerance, absolute_tolerance=None):
262
+ """Check if 2 values are different.
263
+
264
+ In case of numerical values, the tolerance is used to check if the values
265
+ are different.
266
+ In all other cases, the difference is straight forward.
267
+ """
268
+ if first == second:
269
+ # values are same - simple case
270
+ return False
271
+
272
+ first_is_nan, second_is_nan = bool(first != first), bool(second != second)
273
+
274
+ if first_is_nan or second_is_nan:
275
+ # two 'NaN' values are not different (see issue #114)
276
+ return not (first_is_nan and second_is_nan)
277
+ elif isinstance(first, (int, float)) and isinstance(second, (int, float)):
278
+ # two numerical values are compared with tolerance
279
+ return not isclose(
280
+ first,
281
+ second,
282
+ rel_tol=tolerance or 0,
283
+ abs_tol=absolute_tolerance or 0,
284
+ )
285
+ # we got different values
286
+ return True
287
+
288
+
289
+ def strip_removed_objects(obj):
290
+ """Strip all the values that are instances of ``RemovedObject``."""
291
+ if isinstance(obj, LIST_TYPES):
292
+ return [
293
+ strip_removed_objects(value)
294
+ for value in obj
295
+ if not isinstance(value, RemovedObject)
296
+ ]
297
+ if isinstance(obj, DICT_TYPES):
298
+ new_dict = {}
299
+ for key, value in obj.items():
300
+ if isinstance(value, RemovedObject):
301
+ continue
302
+ clean_value = strip_removed_objects(value)
303
+ new_dict[key] = clean_value
304
+ return new_dict
305
+ return obj
306
+
307
+
308
+ def isclose(a, b, rel_tol=1e-09, abs_tol=0.0):
309
+ """Verify if two values are close to each other."""
310
+ return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
@@ -0,0 +1,4 @@
1
+ # Keep this version synchronized with pyproject.toml using bump2version.
2
+ """Version information for dictdiffer package."""
3
+
4
+ __version__ = "0.0.7"
@@ -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,12 @@
1
+ inspire_dictdiffer/__init__.py,sha256=5ML-eb_IdJVR3JPQ0cbw8mqJlUsyi0XnIoR-EZQczpw,15170
2
+ inspire_dictdiffer/conflict.py,sha256=q9Ia9yNMo6_IBIiy62q8gJmEbP1bVVabUDandVMBgwM,2401
3
+ inspire_dictdiffer/merge.py,sha256=04CaFpKJj4tM3j_NaYNoutZYdAX-IRuOf7kKMGZ3Ol0,5119
4
+ inspire_dictdiffer/resolve.py,sha256=WRT5uaHbsz6dejTCZMvqwdaXRFg33VY-oUlhuUmuaoM,5373
5
+ inspire_dictdiffer/testing.py,sha256=tOyY-8uu-sZ-do-mD8w7PZv-ksObohpnEsrELrG637o,1122
6
+ inspire_dictdiffer/unify.py,sha256=l39HpLoI8aYv6LG62Gp39vdp6U1GvxeSvr7clZwscJE,1777
7
+ inspire_dictdiffer/utils.py,sha256=Ia2R33-OUpsBl1yC9IDEIgYk7k_k_BYJ2ChEhJAgUoc,9562
8
+ inspire_dictdiffer/version.py,sha256=gnhfK7jNsnB4dCOPVrnkLhkLHWUM6RZcGykwbNCvlHI,146
9
+ pl_dict_diff-0.0.1.dist-info/licenses/LICENSE,sha256=H54Fs7mm-tJMxnRVV4vN_T6JsJolxFNppSVCA7vivdM,1398
10
+ pl_dict_diff-0.0.1.dist-info/METADATA,sha256=q4B2_ajXRONIYzbymi4RICy7KIrD6VtXspxmI3czwcA,3244
11
+ pl_dict_diff-0.0.1.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
12
+ pl_dict_diff-0.0.1.dist-info/RECORD,,