diff --git a/dpath/__init__.py b/dpath/__init__.py index 2bc9942..a3a93b5 100644 --- a/dpath/__init__.py +++ b/dpath/__init__.py @@ -301,7 +301,7 @@ def merger(dst, src, _segments=()): # Our current path in the source. current_path = _segments + (key,) - if len(key) == 0 and not options.ALLOW_EMPTY_STRING_KEYS: + if isinstance(key, (str, bytes)) and len(key) == 0 and not options.ALLOW_EMPTY_STRING_KEYS: raise InvalidKeyName("Empty string keys not allowed without " "dpath.options.ALLOW_EMPTY_STRING_KEYS=True: " f"{current_path}") @@ -313,7 +313,7 @@ def merger(dst, src, _segments=()): tt = type(target) ft = type(found) if tt != ft: - path = separator.join(current_path) + path = separator.join(map(segments.int_str, current_path)) raise TypeError(f"Cannot merge objects of type {tt} and {ft} at {path}") # Path not present in destination, create it. diff --git a/dpath/segments.py b/dpath/segments.py index f1443f0..24d6e0b 100644 --- a/dpath/segments.py +++ b/dpath/segments.py @@ -367,7 +367,7 @@ def set( if isinstance(last_segment, str) and isinstance(current, Sequence) and last_segment.isdecimal(): last_segment = int(last_segment) - if isinstance(last_segment, int): + if isinstance(last_segment, int) and isinstance(current, MutableSequence): extend(current, last_segment) current[last_segment] = value diff --git a/tests/test_merge.py b/tests/test_merge.py index a8b638c..0b93d51 100644 --- a/tests/test_merge.py +++ b/tests/test_merge.py @@ -1,4 +1,6 @@ import copy +from collections import UserDict +from unittest import mock from nose2.tools.such import helper @@ -40,6 +42,61 @@ def test_merge_simple_int(): assert dst["integer"] == src["integer"], "%r != %r" % (dst["integer"], src["integer"]) +def test_merge_integer_keys(): + for mapping in (dict, UserDict): + src = mapping({0: "zero", -1: "new", 2: "two"}) + dst = mapping({-1: "old", "0": "string key", "keep": True}) + + assert dpath.merge(dst, src) is dst + assert dst == {0: "zero", -1: "new", 2: "two", "0": "string key", "keep": True} + + +def test_merge_nested_integer_keys(): + src = {"data": {2: {"value": "new"}, 3: "added"}} + dst = {"data": {2: {"value": "old", "keep": True}, "2": "string key"}} + + dpath.merge(dst, src) + + assert dst == {"data": {2: {"value": "new", "keep": True}, 3: "added", "2": "string key"}} + + +def test_merge_integer_keys_list_modes(): + for mode, expected in ((MergeType.ADDITIVE, [1, 2]), (MergeType.REPLACE, [2])): + src = {0: [2]} + dst = {0: [1]} + + dpath.merge(dst, src, flags=mode | MergeType.TYPESAFE) + + assert dst == {0: expected} + + +def test_merge_typesafe_integer_key_path(): + for separator in ("/", ";"): + src = {"data": {2: {"value": "new"}}} + dst = {"data": {2: {"value": 1}}} + + with helper.assertRaises(TypeError) as raised: + dpath.merge(dst, src, flags=MergeType.ADDITIVE | MergeType.TYPESAFE, separator=separator) + + path = separator.join(("data", "2", "value")) + assert str(raised.exception) == f"Cannot merge objects of type and at {path}" + assert dst == {"data": {2: {"value": 1}}} + + +def test_merge_empty_string_key_policy(): + src = {"data": {"": "new"}} + dst = {"data": {"": "old"}} + + with mock.patch.object(dpath.options, "ALLOW_EMPTY_STRING_KEYS", False): + with helper.assertRaises(dpath.exceptions.InvalidKeyName): + dpath.merge(dst, src) + assert dst == {"data": {"": "old"}} + + with mock.patch.object(dpath.options, "ALLOW_EMPTY_STRING_KEYS", True): + dpath.merge(dst, src) + assert dst == src + + def test_merge_simple_string(): src = { "string": "lol I am a string",