From 845ad596ee86ededbf026c1b7352204140abd61a Mon Sep 17 00:00:00 2001 From: syguts Date: Thu, 1 Apr 2021 16:15:53 +0200 Subject: [PATCH 1/3] Enhance filtering and updating for set method 1. Added possibility to filter not only leaves but all dict structures like in search. Flag parameter "only_leaves" - by default is True and functionality is off (Backward compatibility) 2. Added possibility to update dictionaries (works only with "only_leaves" set to False and dictionaries nodes). With this flage we can update specified node (dictionary) --- dpath/util.py | 10 +++- tests/test_util_set.py | 105 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/dpath/util.py b/dpath/util.py index f90fd6e..17127e1 100644 --- a/dpath/util.py +++ b/dpath/util.py @@ -120,7 +120,7 @@ def f(obj, pair, counter): return deleted -def set(obj, glob, value, separator='/', afilter=None): +def set(obj, glob, value, separator='/', afilter=None, only_leaves=True, update_dict=False): ''' Given a path glob, set all existing elements in the document to the given value. Returns the number of elements changed. @@ -135,9 +135,15 @@ def f(obj, pair, counter): return matched = dpath.segments.match(segments, globlist) - selected = afilter and dpath.segments.leaf(found) and afilter(found) + if only_leaves: + selected = afilter and dpath.segments.leaf(found) and afilter(found) + else: + selected = afilter and afilter(found) if (matched and not afilter) or (matched and selected): + nonlocal value + if update_dict and isinstance(value, dict): + value = {**found, **value} dpath.segments.set(obj, segments, value, creator=None) counter[0] += 1 diff --git a/tests/test_util_set.py b/tests/test_util_set.py index 3684a56..e646bcf 100644 --- a/tests/test_util_set.py +++ b/tests/test_util_set.py @@ -1,3 +1,5 @@ +from functools import partial + import dpath.util @@ -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.util.set(dict, '/a/b', 1, update_dict=True) + assert(dict['a']['b'] == 1) + + dict['a']['b'] = 0 + dpath.util.set(dict, ['a', 'b'], 1, update_dict=True) + assert(dict['a']['b'] == 1) + + def test_set_existing_list(): dict = { "a": [ @@ -79,6 +96,94 @@ 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": { + "name": "value", + "b_nested": { + "name": "value", + }, + }, + "c": 1, + "d": 31, + } + } + new_value = "new_value" + + dpath.util.set(dict_obj, '/a/b/*', new_value, afilter=partial(afilter, "name", "value"), only_leaves=False) + + assert dict_obj["a"]["b"]["b_nested"] == new_value + + dict_obj = { + "a": { + "b": { + "name": "value", + "b_nested": { + "name": "value", + }, + }, + "c": 1, + "d": 31, + } + } + new_value = "new_value" + + dpath.util.set(dict_obj, ["a", "*"], new_value, afilter=partial(afilter, "name", "value"), only_leaves=False) + + assert dict_obj["a"]["b"] == 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 + + nested_value = "nested_value" + new_dict_for_update = { + "name": "updated_value" + } + dict_obj = { + "a": { + "b": { + "name": "value", + "b_nested": { + "name": nested_value, + }, + }, + "c": 1, + "d": 31, + } + } + + dpath.util.set(dict_obj, '/a/*', new_dict_for_update, afilter=partial(afilter, "name", "value"), only_leaves=False, + update_dict=True) + + assert dict_obj["a"]["b"]["b_nested"]["name"] == nested_value + assert dict_obj["a"]["b"]["name"] == new_dict_for_update["name"] + + dict_obj = { + "a": { + "b": { + "name": "value", + "b_nested": { + "name": nested_value, + }, + }, + "c": 1, + "d": 31, + } + } + + dpath.util.set(dict_obj, ["a", "*"], new_dict_for_update, afilter=partial(afilter, "name", "value"), + only_leaves=False, update_dict=True) + + assert dict_obj["a"]["b"]["b_nested"]["name"] == nested_value + assert dict_obj["a"]["b"]["name"] == new_dict_for_update["name"] + + def test_set_existing_path_with_separator(): dict = { "a": { From 2e277a426d26f9a3bdaf64f2536c0f31bf8aeaf5 Mon Sep 17 00:00:00 2001 From: syguts Date: Sun, 4 Dec 2022 19:50:19 +0100 Subject: [PATCH 2/3] Enhance filtering and updating for set method 1. Adjusted to the latest version of master branch. 2. Refactored one method '_split_path' to be more pythonic and concise --- dpath/__init__.py | 25 ++++++--- dpath/util.py | 20 ++++++- tests/test_set.py | 129 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 10 deletions(-) diff --git a/dpath/__init__.py b/dpath/__init__.py index 9f56e6b..d1c0a33 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,15 @@ def f(obj, pair, counter): return matched = segments.match(path_segments, globlist) - selected = afilter and segments.leaf(found) and afilter(found) + 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): + 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..d9cd017 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,118 @@ 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": { + "name": "value", + "b_nested": { + "name": "value", + }, + }, + "c": 1, + "d": 31, + } + } + new_value = "new_value" + + dpath.set( + dict_obj, + '/a/b/*', + new_value, + afilter=partial(afilter, "name", "value"), + is_only_leaves_filter=False + ) + + assert dict_obj["a"]["b"]["b_nested"] == new_value + + dict_obj = { + "a": { + "b": { + "name": "value", + "b_nested": { + "name": "value", + }, + }, + "c": 1, + "d": 31, + } + } + new_value = "new_value" + + dpath.set( + dict_obj, + ["a", "*"], + new_value, + afilter=partial(afilter, "name", "value"), + is_only_leaves_filter=False + ) + + assert dict_obj["a"]["b"] == 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 + + nested_value = "nested_value" + new_dict_for_update = { + "name": "updated_value" + } + dict_obj = { + "a": { + "b": { + "name": "value", + "b_nested": { + "name": nested_value, + }, + }, + "c": 1, + "d": 31, + } + } + + dpath.set( + dict_obj, + '/a/*', + new_dict_for_update, + afilter=partial(afilter, "name", "value"), + is_only_leaves_filter=False, + is_dict_update=True + ) + + assert dict_obj["a"]["b"]["b_nested"]["name"] == nested_value + assert dict_obj["a"]["b"]["name"] == new_dict_for_update["name"] + + dict_obj = { + "a": { + "b": { + "name": "value", + "b_nested": { + "name": nested_value, + }, + }, + "c": 1, + "d": 31, + } + } + + dpath.set( + dict_obj, + ["a", "*"], + new_dict_for_update, + afilter=partial(afilter, "name", "value"), + is_only_leaves_filter=False, + is_dict_update=True + ) + + assert dict_obj["a"]["b"]["b_nested"]["name"] == nested_value + assert dict_obj["a"]["b"]["name"] == new_dict_for_update["name"] + + def test_set_existing_path_with_separator(): dict = { "a": { From 7bb6a1456910c2126470e7a573850876b88b5226 Mon Sep 17 00:00:00 2001 From: syguts Date: Sun, 4 Dec 2022 21:17:52 +0100 Subject: [PATCH 3/3] Enhance filtering and updating for set method 1. Fixed a small bug under new logic. 2. Provided more self-explained tests. --- dpath/__init__.py | 6 ++++- tests/test_set.py | 59 +++++++++++++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/dpath/__init__.py b/dpath/__init__.py index d1c0a33..b22b3b9 100644 --- a/dpath/__init__.py +++ b/dpath/__init__.py @@ -150,7 +150,11 @@ def f(obj, pair, counter): else: selected = afilter and afilter(found) - if (matched and not afilter) or (matched and selected): + 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} diff --git a/tests/test_set.py b/tests/test_set.py index d9cd017..9c92ab9 100644 --- a/tests/test_set.py +++ b/tests/test_set.py @@ -103,13 +103,19 @@ def afilter(key, value, x): dict_obj = { "a": { "b": { - "name": "value", + "some_known_key": "some_known_key_value", "b_nested": { "name": "value", }, }, "c": 1, - "d": 31, + "d": { + "some_known_key": "some_other_known_key_value", + "b_nested": { + "name": "value", + }, + }, + "e": 31, } } new_value = "new_value" @@ -118,52 +124,61 @@ def afilter(key, value, x): dict_obj, '/a/b/*', new_value, - afilter=partial(afilter, "name", "value"), + afilter=partial(afilter, "some_known_key", "some_other_known_key_value"), is_only_leaves_filter=False ) - - assert dict_obj["a"]["b"]["b_nested"] == new_value + assert dict_obj["a"]["b"]["b_nested"]["name"] == "value" + assert dict_obj["a"]["d"] == new_value dict_obj = { "a": { "b": { - "name": "value", + "some_known_key": "some_known_key_value", "b_nested": { "name": "value", }, }, "c": 1, - "d": 31, + "d": { + "some_known_key": "some_other_known_key_value", + "b_nested": { + "name": "value", + }, + }, + "e": 31, } } new_value = "new_value" dpath.set( dict_obj, - ["a", "*"], + ["a", "b", "*"], new_value, - afilter=partial(afilter, "name", "value"), + afilter=partial(afilter, "some_known_key", "some_other_known_key_value"), is_only_leaves_filter=False ) - assert dict_obj["a"]["b"] == new_value + 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 - nested_value = "nested_value" new_dict_for_update = { - "name": "updated_value" + "name": "updated_value", + "some_other_value": 100 } dict_obj = { "a": { "b": { - "name": "value", + "some_known_key": "some_known_key_value", + "name": "some_name", "b_nested": { - "name": nested_value, + "name": "some_nested_value", }, + "some_other_value": 33 }, "c": 1, "d": 31, @@ -174,21 +189,24 @@ def afilter(key, value, x): dict_obj, '/a/*', new_dict_for_update, - afilter=partial(afilter, "name", "value"), + afilter=partial(afilter, "some_known_key", "some_known_key_value"), is_only_leaves_filter=False, is_dict_update=True ) - assert dict_obj["a"]["b"]["b_nested"]["name"] == nested_value 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": { - "name": "value", + "some_known_key": "some_known_key_value", + "name": "some_name", "b_nested": { - "name": nested_value, + "name": "some_nested_value", }, + "some_other_value": 33 }, "c": 1, "d": 31, @@ -199,13 +217,14 @@ def afilter(key, value, x): dict_obj, ["a", "*"], new_dict_for_update, - afilter=partial(afilter, "name", "value"), + afilter=partial(afilter, "some_known_key", "some_known_key_value"), is_only_leaves_filter=False, is_dict_update=True ) - assert dict_obj["a"]["b"]["b_nested"]["name"] == nested_value 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():