Skip to content

Commit

Permalink
Catch more edge cases, add corresponding tests
Browse files Browse the repository at this point in the history
  • Loading branch information
teutoburg committed Nov 8, 2023
1 parent c7ffaa8 commit f6c5a5b
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 10 deletions.
29 changes: 25 additions & 4 deletions astar_utils/nested_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
from typing import TextIO
from io import StringIO
from collections.abc import Iterable, Mapping, MutableMapping
from collections.abc import Iterable, Sequence, Mapping, MutableMapping

from more_itertools import ilen

Expand Down Expand Up @@ -32,6 +32,9 @@ def update(self, new_dict: MutableMapping) -> None:
new_dict["properties"])
else:
self.dic[alias] = new_dict["properties"]
elif isinstance(new_dict, Sequence):
# To catch list of tuples
self.update(dict([new_dict]))
else:
# Catch any bang-string properties keys
to_pop = []
Expand All @@ -52,8 +55,11 @@ def __getitem__(self, key: str):
entry = self.dic
for chunk in key_chunks:
if not isinstance(entry, Mapping):
raise KeyError(chunk)
entry = entry[chunk]
raise KeyError(key)
try:
entry = entry[chunk]
except KeyError as err:
raise KeyError(key) from err
return entry
return self.dic[key]

Expand All @@ -66,6 +72,7 @@ def __setitem__(self, key: str, value) -> None:
if chunk not in entry:
entry[chunk] = {}
entry = entry[chunk]
self._guard_submapping(entry, key_chunks, "set")
entry[final_key] = value
else:
self.dic[key] = value
Expand All @@ -77,8 +84,9 @@ def __delitem__(self, key: str) -> None:
entry = self.dic
for chunk in key_chunks:
if not isinstance(entry, Mapping):
raise KeyError(chunk)
raise KeyError(key)

Check warning on line 87 in astar_utils/nested_mapping.py

View check run for this annotation

Codecov / codecov/patch

astar_utils/nested_mapping.py#L87

Added line #L87 was not covered by tests
entry = entry[chunk]
self._guard_submapping(entry, key_chunks, "del")
del entry[final_key]
else:
del self.dic[key]
Expand All @@ -93,6 +101,19 @@ def _join_subkey(key=None, subkey=None) -> str:
# TODO: py39: item.removeprefix("!")
return f"!{key.strip('!')}.{subkey}" if key is not None else subkey

@staticmethod
def _guard_submapping(entry, key_chunks, kind: str = "set") -> None:
kinds = {"set": "overwritten with a new sub-mapping",
"del": "be deleted from"}
submsg = kinds.get(kind, "modified")
if not isinstance(entry, Mapping):
raise KeyError(
f"Bang-key '!{'.'.join(key_chunks)}' doesn't point to a sub-"
f"mapping but to a single value, which cannot be {submsg}. "
"To replace or remove the value, call ``del "
f"self['!{'.'.join(key_chunks)}']`` first and then optionally "
"re-assign a new sub-mapping to the key.")

def _yield_subkeys(self, key: str, value: Mapping):
# TODO: py39: -> Iterator[str]
for subkey, subvalue in value.items():
Expand Down
37 changes: 31 additions & 6 deletions tests/test_nested_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,18 @@ def test_initialises_with_nothing(self):
assert isinstance(NestedMapping(), NestedMapping)

def test_initalises_with_normal_dict(self):
nestmap = NestedMapping({"a": 1})
nestmap = NestedMapping({"a": 1, "b": 2})
assert isinstance(nestmap, NestedMapping)

def test_initalises_with_list_of_tuples(self):
nestmap = NestedMapping([("a", 1), ("b", 2)])
assert isinstance(nestmap, NestedMapping)

def test_initalises_with_basic_yaml_dict(self, basic_nestmap):
assert isinstance(basic_nestmap, NestedMapping)
assert "OBS" in basic_nestmap.dic

def test_initalises_with_nested_yaml_dict(self, nested_nestmap):
def test_initalises_with_nested_dict(self, nested_nestmap):
assert isinstance(nested_nestmap, NestedMapping)
assert "moo" in nested_nestmap.dic

Expand Down Expand Up @@ -79,10 +83,26 @@ def test_can_delete_simple_key(self, nested_nestmap):
assert "foo" not in nested_nestmap

def test_can_delete_nested_key(self, nested_nestmap):
assert nested_nestmap["!bar.bogus.b"] == 69
del nested_nestmap["!bar.bogus.b"]
with pytest.raises(KeyError):
nested_nestmap["!bar.bogus.b"]
key = "!bar.bogus.b"
assert nested_nestmap[key] == 69
del nested_nestmap[key]
with pytest.raises(KeyError) as excinfo:
nested_nestmap[key]
assert key in str(excinfo.value)

@pytest.mark.parametrize("key", ["bogus", "!foo.bogus"])
def test_throws_when_deleting_nonexisting_key(self, key, nested_nestmap):
with pytest.raises(KeyError) as excinfo:
del nested_nestmap[key]
if key.startswith("!"):
assert "deleted from" in str(excinfo.value)
else:
assert key in str(excinfo.value)

def test_throws_when_setting_value_to_subdict(self, nested_nestmap):
with pytest.raises(KeyError) as excinfo:
nested_nestmap["!foo.bogus"] = 3
assert "overwritten with" in str(excinfo.value)


class TestRecursiveUpdate:
Expand All @@ -100,6 +120,11 @@ def test_updates_yaml_alias_recursive_dicts(self, basic_nestmap,
assert basic_nestmap["!OBS.temperature"] == 42
assert basic_nestmap["OBS"]["humidity"] == 0.75

def test_updates_yaml_bang_properties_dicts(self, basic_nestmap,
basic_yaml):
basic_nestmap.update({"!SIM.someglobal": True})
assert basic_nestmap["!SIM.someglobal"]


class TestFunctionRecursiveUpdate:
def test_recursive_update_combines_dicts(self):
Expand Down

0 comments on commit f6c5a5b

Please sign in to comment.