diff --git a/dpath/__init__.py b/dpath/__init__.py index 9f56e6b..b22b3b9 100644 --- a/dpath/__init__.py +++ b/dpath/__init__.py @@ -40,12 +40,7 @@ def _split_path(path: Path, separator: Optional[str] = "/") -> Union[List[PathSe ignored, and is assumed to be part of each key glob. It will not be stripped. """ - if not segments.leaf(path): - split_segments = path - else: - split_segments = path.lstrip(separator).split(separator) - - return split_segments + return path.lstrip(separator).split(separator) if segments.leaf(path) else path def new(obj: MutableMapping, path: Path, value, separator="/", creator: Creator = None) -> MutableMapping: @@ -127,7 +122,15 @@ def f(obj, pair, counter): return deleted -def set(obj: MutableMapping, glob: Glob, value, separator="/", afilter: Filter = None) -> int: +def set( + obj: MutableMapping, + glob: Glob, + value, + separator="/", + afilter: Filter = None, + is_only_leaves_filter: bool = True, + is_dict_update: bool = False +) -> int: """ Given a path glob, set all existing elements in the document to the given value. Returns the number of elements changed. @@ -142,9 +145,19 @@ def f(obj, pair, counter): return matched = segments.match(path_segments, globlist) - selected = afilter and segments.leaf(found) and afilter(found) - - if (matched and not afilter) or (matched and selected): + if is_only_leaves_filter: + selected = afilter and segments.leaf(found) and afilter(found) + else: + selected = afilter and afilter(found) + + if ( + (matched and not afilter) or + (matched and selected) or + (selected and not is_only_leaves_filter) + ): + nonlocal value + if is_dict_update and isinstance(value, dict): + value = {**found, **value} segments.set(obj, path_segments, value, creator=None) counter[0] += 1 diff --git a/dpath/util.py b/dpath/util.py index 60d0319..f5f5afa 100644 --- a/dpath/util.py +++ b/dpath/util.py @@ -27,8 +27,24 @@ def delete(obj, glob, separator="/", afilter=None): @deprecated -def set(obj, glob, value, separator="/", afilter=None): - return dpath.set(obj, glob, value, separator, afilter) +def set( + obj, + glob, + value, + separator="/", + afilter=None, + is_only_leaves_filter: bool = True, + is_dict_update: bool = False +): + return dpath.set( + obj, + glob, + value, + separator, + afilter, + is_only_leaves_filter, + is_dict_update + ) @deprecated diff --git a/tests/test_set.py b/tests/test_set.py index ef2dd96..9c92ab9 100644 --- a/tests/test_set.py +++ b/tests/test_set.py @@ -1,3 +1,5 @@ +from functools import partial + import dpath @@ -31,6 +33,21 @@ def test_set_existing_dict(): assert dict['a']['b'] == 1 +def test_set_existing_dict_not_affected_by_update_dict_flag(): + dict = { + "a": { + "b": 0, + }, + } + + dpath.set(dict, '/a/b', 1, is_dict_update=True) + assert (dict['a']['b'] == 1) + + dict['a']['b'] = 0 + dpath.set(dict, ['a', 'b'], 1, is_dict_update=True) + assert (dict['a']['b'] == 1) + + def test_set_existing_list(): dict = { "a": [ @@ -79,6 +96,137 @@ def afilter(x): assert dict['a']['d'] == 31337 +def test_set_filter_not_only_leaves(): + def afilter(key, value, x): + return isinstance(x, dict) and x.get(key) == value + + dict_obj = { + "a": { + "b": { + "some_known_key": "some_known_key_value", + "b_nested": { + "name": "value", + }, + }, + "c": 1, + "d": { + "some_known_key": "some_other_known_key_value", + "b_nested": { + "name": "value", + }, + }, + "e": 31, + } + } + new_value = "new_value" + + dpath.set( + dict_obj, + '/a/b/*', + new_value, + afilter=partial(afilter, "some_known_key", "some_other_known_key_value"), + is_only_leaves_filter=False + ) + assert dict_obj["a"]["b"]["b_nested"]["name"] == "value" + assert dict_obj["a"]["d"] == new_value + + dict_obj = { + "a": { + "b": { + "some_known_key": "some_known_key_value", + "b_nested": { + "name": "value", + }, + }, + "c": 1, + "d": { + "some_known_key": "some_other_known_key_value", + "b_nested": { + "name": "value", + }, + }, + "e": 31, + } + } + new_value = "new_value" + + dpath.set( + dict_obj, + ["a", "b", "*"], + new_value, + afilter=partial(afilter, "some_known_key", "some_other_known_key_value"), + is_only_leaves_filter=False + ) + + assert dict_obj["a"]["b"]["b_nested"]["name"] == "value" + assert dict_obj["a"]["d"] == new_value + + +def test_set_filter_not_only_leaves_and_update_dict_flag(): + def afilter(key, value, x): + return isinstance(x, dict) and x.get(key) == value + + new_dict_for_update = { + "name": "updated_value", + "some_other_value": 100 + } + dict_obj = { + "a": { + "b": { + "some_known_key": "some_known_key_value", + "name": "some_name", + "b_nested": { + "name": "some_nested_value", + }, + "some_other_value": 33 + }, + "c": 1, + "d": 31, + } + } + + dpath.set( + dict_obj, + '/a/*', + new_dict_for_update, + afilter=partial(afilter, "some_known_key", "some_known_key_value"), + is_only_leaves_filter=False, + is_dict_update=True + ) + + assert dict_obj["a"]["b"]["name"] == new_dict_for_update["name"] + assert dict_obj["a"]["b"]["some_other_value"] == \ + new_dict_for_update["some_other_value"] + + dict_obj = { + "a": { + "b": { + "some_known_key": "some_known_key_value", + "name": "some_name", + "b_nested": { + "name": "some_nested_value", + }, + "some_other_value": 33 + }, + "c": 1, + "d": 31, + } + } + + dpath.set( + dict_obj, + ["a", "*"], + new_dict_for_update, + afilter=partial(afilter, "some_known_key", "some_known_key_value"), + is_only_leaves_filter=False, + is_dict_update=True + ) + + assert dict_obj["a"]["b"]["name"] == new_dict_for_update["name"] + assert dict_obj["a"]["b"]["some_other_value"] == \ + new_dict_for_update["some_other_value"] + + def test_set_existing_path_with_separator(): dict = { "a": {