From f871299e36626d7d3ab4ed8328eab2e283024a51 Mon Sep 17 00:00:00 2001 From: moomoohk <2220203+moomoohk@users.noreply.github.com> Date: Sun, 8 Jan 2023 18:33:03 +0200 Subject: [PATCH 1/6] Better int ambiguity resolution in default creator --- dpath/segments.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/dpath/segments.py b/dpath/segments.py index 7a48817..b2df380 100644 --- a/dpath/segments.py +++ b/dpath/segments.py @@ -1,6 +1,6 @@ from copy import deepcopy from fnmatch import fnmatchcase -from typing import List, Sequence, Tuple, Iterator, Any, Union, Optional, MutableMapping +from typing import List, Sequence, Tuple, Iterator, Any, Union, Optional, MutableMapping, MutableSequence from dpath import options from dpath.exceptions import InvalidGlob, InvalidKeyName, PathNotFound @@ -254,7 +254,7 @@ def match(segments: Path, glob: Glob): return False -def extend(thing: List, index: int, value=None): +def extend(thing: MutableSequence, index: int, value=None): """ Extend a sequence like thing such that it contains at least index + 1 many elements. The extension values will be None (default). @@ -280,7 +280,7 @@ def extend(thing: List, index: int, value=None): def _default_creator( - current: Union[MutableMapping, List], + current: Union[MutableMapping, Sequence], segments: Sequence[PathSegment], i: int, hints: Sequence[Tuple[PathSegment, type]] = () @@ -294,7 +294,10 @@ def _default_creator( segment = segments[i] length = len(segments) - if isinstance(segment, int): + if isinstance(current, Sequence): + segment = int(segment) + + if isinstance(current, MutableSequence): extend(current, segment) # Infer the type from the hints provided. @@ -308,7 +311,7 @@ def _default_creator( else: segment_next = None - if isinstance(segment_next, int): + if isinstance(segment_next, int) or segment_next.isdigit(): current[segment] = [] else: current[segment] = {} From 436dac44bc33558743c29acce2e2bdc2b34307ec Mon Sep 17 00:00:00 2001 From: moomoohk <2220203+moomoohk@users.noreply.github.com> Date: Sun, 8 Jan 2023 18:34:37 +0200 Subject: [PATCH 2/6] Remove unused import --- dpath/segments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpath/segments.py b/dpath/segments.py index b2df380..fa18c8f 100644 --- a/dpath/segments.py +++ b/dpath/segments.py @@ -1,6 +1,6 @@ from copy import deepcopy from fnmatch import fnmatchcase -from typing import List, Sequence, Tuple, Iterator, Any, Union, Optional, MutableMapping, MutableSequence +from typing import Sequence, Tuple, Iterator, Any, Union, Optional, MutableMapping, MutableSequence from dpath import options from dpath.exceptions import InvalidGlob, InvalidKeyName, PathNotFound From b07cc6a9776981e627d224f216c385196e7ace76 Mon Sep 17 00:00:00 2001 From: moomoohk <2220203+moomoohk@users.noreply.github.com> Date: Sun, 8 Jan 2023 18:59:24 +0200 Subject: [PATCH 3/6] Resolve int ambiguity in get function --- dpath/segments.py | 9 ++++++--- dpath/types.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/dpath/segments.py b/dpath/segments.py index fa18c8f..56ac0ed 100644 --- a/dpath/segments.py +++ b/dpath/segments.py @@ -81,7 +81,7 @@ def walk(obj, location=()): yield found -def get(obj, segments): +def get(obj, segments: Path): """ Return the value at the path indicated by segments. @@ -92,6 +92,9 @@ def get(obj, segments): if leaf(current): raise PathNotFound(f"Path: {segments}[{i}]") + if isinstance(current, Sequence) and isinstance(segment, (str, bytes)) and segment.isdigit(): + segment = int(segment) + current = current[segment] return current @@ -339,7 +342,7 @@ def set( for (i, segment) in enumerate(segments[:-1]): # If segment is non-int but supposed to be a sequence index - if isinstance(segment, str) and isinstance(current, Sequence) and segment.isdigit(): + if isinstance(segment, (str, bytes)) and isinstance(current, Sequence) and segment.isdigit(): segment = int(segment) try: @@ -361,7 +364,7 @@ def set( last_segment = segments[-1] # Resolve ambiguity of last segment - if isinstance(last_segment, str) and isinstance(current, Sequence) and last_segment.isdigit(): + if isinstance(last_segment, (str, bytes)) and isinstance(current, Sequence) and last_segment.isdigit(): last_segment = int(last_segment) if isinstance(last_segment, int): diff --git a/dpath/types.py b/dpath/types.py index 7bf3d2d..c4a4a56 100644 --- a/dpath/types.py +++ b/dpath/types.py @@ -46,7 +46,7 @@ class MergeType(IntFlag): replaces the destination in this situation.""" -PathSegment = Union[int, str] +PathSegment = Union[int, str, bytes] """Type alias for dict path segments where integers are explicitly casted.""" Filter = Callable[[Any], bool] From 58b34df1721392006e05e89460f7e33ff4351833 Mon Sep 17 00:00:00 2001 From: moomoohk <2220203+moomoohk@users.noreply.github.com> Date: Sun, 8 Jan 2023 19:33:15 +0200 Subject: [PATCH 4/6] Use isdecimal in favor of isdigit --- dpath/segments.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dpath/segments.py b/dpath/segments.py index 56ac0ed..c3c9846 100644 --- a/dpath/segments.py +++ b/dpath/segments.py @@ -92,7 +92,7 @@ def get(obj, segments: Path): if leaf(current): raise PathNotFound(f"Path: {segments}[{i}]") - if isinstance(current, Sequence) and isinstance(segment, (str, bytes)) and segment.isdigit(): + if isinstance(current, Sequence) and isinstance(segment, str) and segment.isdecimal(): segment = int(segment) current = current[segment] @@ -314,7 +314,7 @@ def _default_creator( else: segment_next = None - if isinstance(segment_next, int) or segment_next.isdigit(): + if isinstance(segment_next, int) or (isinstance(segment_next, str) and segment_next.isdecimal()): current[segment] = [] else: current[segment] = {} @@ -342,7 +342,7 @@ def set( for (i, segment) in enumerate(segments[:-1]): # If segment is non-int but supposed to be a sequence index - if isinstance(segment, (str, bytes)) and isinstance(current, Sequence) and segment.isdigit(): + if isinstance(segment, str) and isinstance(current, Sequence) and segment.isdecimal(): segment = int(segment) try: @@ -364,7 +364,7 @@ def set( last_segment = segments[-1] # Resolve ambiguity of last segment - if isinstance(last_segment, (str, bytes)) and isinstance(current, Sequence) and last_segment.isdigit(): + if isinstance(last_segment, str) and isinstance(current, Sequence) and last_segment.isdecimal(): last_segment = int(last_segment) if isinstance(last_segment, int): From cbed5f143fda3c1c4f9cacdf6bd8af2d2a0383e2 Mon Sep 17 00:00:00 2001 From: moomoohk <2220203+moomoohk@users.noreply.github.com> Date: Sun, 8 Jan 2023 20:49:35 +0200 Subject: [PATCH 5/6] Add type check tests Thanks to @harel --- tests/test_new.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_new.py b/tests/test_new.py index 15b21c6..ac47e7d 100644 --- a/tests/test_new.py +++ b/tests/test_new.py @@ -52,6 +52,27 @@ def test_set_list_with_dict_int_ambiguity(): assert d == expected +def test_int_segment_list_type_check(): + d = {} + dpath.new(d, "a/b/0/c/0", "hello") + assert 'b' in d.get("a", {}) + assert isinstance(d["a"]["b"], list) + assert len(d["a"]["b"]) == 1 + assert 'c' in d["a"]["b"][0] + assert isinstance(d["a"]["b"][0]["c"], list) + assert len(d["a"]["b"][0]["c"]) == 1 + + +def test_int_segment_dict_type_check(): + d = {"a": {"b": {"0": {}}}} + dpath.new(d, "a/b/0/c/0", "hello") + assert "b" in d.get("a", {}) + assert isinstance(d["a"]["b"], dict) + assert '0' in d["a"]["b"] + assert 'c' in d["a"]["b"]["0"] + assert isinstance(d["a"]["b"]["0"]["c"], list) + + def test_set_new_list_path_with_separator(): # This test kills many birds with one stone, forgive me dict = { From 013b3a793c7a81f4577939c95808717f5a570fb1 Mon Sep 17 00:00:00 2001 From: moomoohk <2220203+moomoohk@users.noreply.github.com> Date: Mon, 9 Jan 2023 08:55:27 +0200 Subject: [PATCH 6/6] Update version.py --- dpath/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpath/version.py b/dpath/version.py index 4260069..5dfae46 100644 --- a/dpath/version.py +++ b/dpath/version.py @@ -1 +1 @@ -VERSION = "2.1.3" +VERSION = "2.1.4"