Skip to content
This repository has been archived by the owner on Jun 26, 2021. It is now read-only.

Commit

Permalink
Merge pull request #224 from delira-dev/config_traverse_fix
Browse files Browse the repository at this point in the history
Fix lookup of non existent keys
  • Loading branch information
mibaumgartner authored Oct 9, 2019
2 parents df0a7e4 + 4ae8c80 commit 176acb7
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 27 deletions.
63 changes: 36 additions & 27 deletions delira/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def warning_wrapper(config, key, *args, **kwargs):
Parameters
----------
config: :class:`Config`
decorated function receive :param`self` as first argument
decorated function receive :param:`self` as first argument
key : immutable type
key which is checked
Expand Down Expand Up @@ -134,7 +134,11 @@ def _traverse_keys(self, keys, create=False):
current_level = self
for k in keys:
if k not in current_level:
current_level[k] = self._create_internal_dict()
if create:
current_level[k] = self._create_internal_dict()
else:
raise KeyError(
"{} was not found in internal dict.".format(k))
# traverse to needed dict
current_level = current_level[k]
return current_level
Expand Down Expand Up @@ -249,7 +253,7 @@ def update(self, update_dict, deepcopy=False, overwrite=False):
update_dict : dictlike
values which should be added to config
deepcopy : bool, optional
copies values from :param`update_dict`, by default False
copies values from :param:`update_dict`, by default False
overwrite : bool, optional
overwrite existing values inside config, by default False
Expand All @@ -274,7 +278,7 @@ def _update(self, key, item, deepcopy=False, overwrite=False):
item : Any
item which should be assigned
deepcopy : bool, optional
copies :param`item`, by default False
copies :param:`item`, by default False
overwrite : bool, optional
overwrite existing values inside config, by default False
"""
Expand Down Expand Up @@ -322,9 +326,9 @@ def dump(self, path, formatter=yaml.dump, encoder_cls=Encoder, **kwargs):
defines the format how the config is saved, by default yaml.dump
encoder_cls : :class:`Encoder`, optional
transforms config to a format which can be formatted by the
:param`formatter`, by default Encoder
:param:`formatter`, by default Encoder
kwargs:
additional keyword arguments passed to :param`formatter`
additional keyword arguments passed to :param:`formatter`
"""
self._timestamp = now()
encoded_self = encoder_cls().encode(self)
Expand All @@ -342,9 +346,9 @@ def dumps(self, formatter=yaml.dump, encoder_cls=Encoder, **kwargs):
defines the format how the config is saved, by default yaml.dump
encoder_cls : :class:`Encoder`, optional
transforms config to a format which can be formatted by the
:param`formatter`, by default Encoder
:param:`formatter`, by default Encoder
kwargs:
additional keyword arguments passed to :param`formatter`
additional keyword arguments passed to :param:`formatter`
"""
self._timestamp = now()
encoded_self = encoder_cls().encode(self)
Expand All @@ -362,9 +366,9 @@ def load(self, path, formatter=yaml.load, decoder_cls=Decoder, **kwargs):
defines the format how the config is saved, by default yaml.dump
decoder_cls : :class:`Encoder`, optional
transforms config to a format which can be formatted by the
:param`formatter`, by default Encoder
:param:`formatter`, by default Encoder
kwargs:
additional keyword arguments passed to :param`formatter`
additional keyword arguments passed to :param:`formatter`
"""
with open(path, "r") as f:
decoded_format = formatter(f, **kwargs)
Expand All @@ -383,9 +387,9 @@ def loads(self, data, formatter=yaml.load, decoder_cls=Decoder, **kwargs):
defines the format how the config is saved, by default yaml.dump
decoder_cls : :class:`Encoder`, optional
transforms config to a format which can be formatted by the
:param`formatter`, by default Encoder
:param:`formatter`, by default Encoder
kwargs:
additional keyword arguments passed to :param`formatter`
additional keyword arguments passed to :param:`formatter`
"""
decoded_format = formatter(data, **kwargs)
decoded_format = decoder_cls().decode(decoded_format)
Expand All @@ -411,7 +415,7 @@ def create_from_dict(cls, value, deepcopy=False):
Raises
------
TypeError
raised if :param`value` is not a dict (or a subclass of dict)
raised if :param:`value` is not a dict (or a subclass of dict)
"""
if not isinstance(value, dict):
raise TypeError("Value must be an instance of dict but type {} "
Expand Down Expand Up @@ -467,9 +471,9 @@ def create_from_file(cls, path, formatter=yaml.load, decoder_cls=Decoder,
defines the format how the config is saved, by default yaml.dump
decoder_cls : :class:`Encoder`, optional
trasforms config to a format which can be formatted by the
:param`formatter`, by default Encoder
:param:`formatter`, by default Encoder
kwargs:
additional keyword arguments passed to :param`formatter`
additional keyword arguments passed to :param:`formatter`
Returns
-------
Expand All @@ -495,9 +499,9 @@ def create_from_str(cls, data, formatter=yaml.load, decoder_cls=Decoder,
defines the format how the config is saved, by default yaml.dump
decoder_cls : :class:`Encoder`, optional
trasforms config to a format which can be formatted by the
:param`formatter`, by default Encoder
:param:`formatter`, by default Encoder
kwargs:
additional keyword arguments passed to :param`formatter`
additional keyword arguments passed to :param:`formatter`
Returns
-------
Expand Down Expand Up @@ -546,29 +550,31 @@ def __contains__(self, key):
"""
contain = True
try:
self.nested_get(key)
self.nested_get(key, allow_multiple=True)
except KeyError:
contain = False
return contain

def nested_get(self, key, *args, **kwargs):
def nested_get(self, key, *args, allow_multiple=False, **kwargs):
"""
Returns all occurances of :param`key` in :param`self` and subdicts
Returns all occurances of :param:`key` in :param:`self` and subdicts
Parameters
----------
key : str
the key to search for
*args :
positional arguments to provide default value
allow_multiple: bool
allow multiple results
**kwargs :
keyword arguments to provide default value
Raises
------
KeyError
Multiple Values are found for key
(unclear which value should be returned)
Multiple Values are found for key and :param:`allow_multiple` is
False (unclear which value should be returned)
OR
No Value was found for key and no default value was given
Expand All @@ -583,7 +589,10 @@ def nested_get(self, key, *args, **kwargs):
return self[key]
results = nested_lookup(key, self)
if len(results) > 1:
raise KeyError("Multiple Values found for key %s" % key)
if allow_multiple:
return results
else:
raise KeyError("Multiple Values found for key %s" % key)
elif len(results) == 0:
if "default" in kwargs:
return kwargs["default"]
Expand Down Expand Up @@ -689,7 +698,7 @@ def variable_params(self, new_params: dict):
Raises
------
TypeError
raised if :param`new_params` is not a dict (or a subclass of dict)
raised if :param:`new_params` is not a dict (or a subclass of dict)
"""
if not isinstance(new_params, dict):
raise TypeError("new_params must be an instance of dict but "
Expand Down Expand Up @@ -727,7 +736,7 @@ def fixed_params(self, new_params: dict):
Raises
------
TypeError
raised if :param`new_params` is not a dict (or a subclass of dict)
raised if :param:`new_params` is not a dict (or a subclass of dict)
"""
if not isinstance(new_params, dict):
raise TypeError("new_params must be an instance of dict but "
Expand Down Expand Up @@ -764,7 +773,7 @@ def model_params(self, new_params: dict):
Raises
------
TypeError
raised if :param`new_params` is not a dict (or a subclass of dict)
raised if :param:`new_params` is not a dict (or a subclass of dict)
"""
if not isinstance(new_params, dict):
raise TypeError("new_params must be an instance of dict but "
Expand Down Expand Up @@ -801,7 +810,7 @@ def training_params(self, new_params: dict):
Raises
------
TypeError
raised if :param`new_params` is not a dict (or a subclass of dict)
raised if :param:`new_params` is not a dict (or a subclass of dict)
"""
if not isinstance(new_params, dict):
raise TypeError("new_params must be an instance of dict but "
Expand Down
20 changes: 20 additions & 0 deletions tests/utils/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ def test_config_access(self):
with self.assertWarns(RuntimeWarning, msg=warning_msg):
cf[5] = 10

@unittest.skipUnless(
check_for_no_backend(),
"Test should only be executed if no backend is specified")
def test_config_access_with_non_existing_keys(self):
cf = self.config_cls(self.example_dict)

with self.assertRaises(KeyError):
cf["unknown_key"]

with self.assertRaises(KeyError):
cf["shallowStr.unknown_key"]

@unittest.skipUnless(
check_for_no_backend(),
"Test should only be executed if no backend is specified")
Expand Down Expand Up @@ -228,6 +240,14 @@ def test_nested_lookpup(self):
self.assertIsNone(cf.nested_get("nonExistingKey", None))
self.assertIsNone(cf.nested_get("nonExistingKey", default=None))

cf["nested_duplicate.deep"] = "duplicate"
with self.assertRaises(KeyError):
cf.nested_get("deep")

multiple_val = cf.nested_get("deep", allow_multiple=True)
self.assertEqual(multiple_val, [{"deepStr": "b", "deepNum": 2},
"duplicate"])


class DeliraConfigTest(LookupConfigTest):
def setUp(self):
Expand Down

0 comments on commit 176acb7

Please sign in to comment.