Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# DeepDiff Change log

- Unreleased
- Preserve original string key casing in paths for matched dictionary keys when `ignore_string_case=True` (#515). `verbose_level=2` and Delta include the second input's spelling in `new_path` for value and type changes. Forward and reverse Delta application use the respective input paths without renaming keys whose casing is ignored. Exact and regex exclusions can use the original paths when comparison reaches those keys; the existing `threshold_to_diff_deeper` shortcut is unchanged. Numeric and bytes key normalization is unchanged.
- Fixed missing type changes between equal-comparing items inside ordered iterables, e.g. `DeepDiff([2], [2.0])` now reports the `int` → `float` change like `DeepDiff(2, 2.0)` and `DeepDiff({'a': 2}, {'a': 2.0})` already do (issue #605).

- v9-1-0
Expand Down
6 changes: 4 additions & 2 deletions deepdiff/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -813,13 +813,15 @@ def _diff_dict(
continue
parents_ids_added = add_to_frozen_set(parents_ids, item_id)

# Case normalization is for matching, not for paths into the input objects.
preserve_case = self.ignore_string_case and isinstance(key1, str) and isinstance(key2, str)
# Go one level deeper
next_level = level.branch_deeper(
t1[key1],
t2[key2],
child_relationship_class=rel_class,
child_relationship_param=key,
child_relationship_param2=key,
child_relationship_param=key1 if preserve_case else key,
child_relationship_param2=key2 if preserve_case else key,
)
self._diff(next_level, parents_ids_added, local_tree=local_tree)

Expand Down
13 changes: 13 additions & 0 deletions tests/test_delta.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@

class TestBasicsOfDelta:

@pytest.mark.parametrize('new_value', [2, 'two'])
def test_ignore_case_delta_uses_original_paths_in_both_directions(self, new_value):
t1 = {'User': {'AboutMe': 1}}
t2 = {'user': {'aboutme': new_value}}
diff = DeepDiff(t1, t2, ignore_string_case=True)
delta = Delta(diff, bidirectional=True, raise_errors=True)

# Ignored casing differences do not rename keys in either input.
assert t1 + delta == {'User': {'AboutMe': new_value}}
assert t2 - delta == {'user': {'aboutme': 1}}
assert t1 == {'User': {'AboutMe': 1}}
assert t2 == {'user': {'aboutme': new_value}}

def test_from_null_delta_json(self):
t1 = None
t2 = [1, 2, 3, 5]
Expand Down
44 changes: 44 additions & 0 deletions tests/test_diff_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,50 @@ def test_string_dict_key_ignore_case(self):
result = {}
assert result == ddiff

def test_ignore_case_preserves_original_dictionary_paths(self):
t1 = {'User': {'AboutMe': 1}}
t2 = {'user': {'aboutme': 2}}
diff = DeepDiff(t1, t2, ignore_string_case=True)
assert diff == {'values_changed': {
"root['User']['AboutMe']": {'old_value': 1, 'new_value': 2}
}}
tree = DeepDiff(t1, t2, ignore_string_case=True, view='tree')
change = next(iter(tree['values_changed']))
assert change.path() == "root['User']['AboutMe']"
assert change.path(use_t2=True) == "root['user']['aboutme']"

@pytest.mark.parametrize('path_option', ['exclude_paths', 'exclude_regex_paths'])
def test_ignore_case_excludes_original_dictionary_path(self, path_option):
t1 = {'User': {'AboutMe': 1}}
t2 = {'User': {'AboutMe': 2}}
path = "root['User']['AboutMe']"
if path_option == 'exclude_regex_paths':
path = r"root\['User'\]\['AboutMe'\]$"
assert not DeepDiff(t1, t2, ignore_string_case=True, **{path_option: [path]})

@pytest.mark.parametrize('new_value,change_type', [(2, 'values_changed'), ('two', 'type_changes')])
def test_ignore_case_verbose_paths_use_each_dictionary_spelling(self, new_value, change_type):
t1 = {'User': {'AboutMe': 1}}
t2 = {'user': {'aboutme': new_value}}
diff = DeepDiff(t1, t2, ignore_string_case=True, verbose_level=2)
expected = {
'old_value': 1,
'new_value': new_value,
'new_path': "root['user']['aboutme']",
}
if change_type == 'type_changes':
expected.update(old_type=int, new_type=str)
assert diff == {change_type: {"root['User']['AboutMe']": expected}}

def test_ignore_case_preserves_paths_for_nested_additions_and_removals(self):
t1 = {'User': {'Keep': 1, 'Removed': 2}}
t2 = {'user': {'keep': 1, 'Added': 3}}
diff = DeepDiff(t1, t2, ignore_string_case=True, threshold_to_diff_deeper=0)
assert diff == {
'dictionary_item_added': ["root['User']['Added']"],
'dictionary_item_removed': ["root['User']['Removed']"],
}

def test_string_list_ignore_case(self):
t1 = ['AboutMe', 'ALIAS']
t2 = ['aboutme', 'alias']
Expand Down