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

Commit 176acb7

Browse files
Merge pull request #224 from delira-dev/config_traverse_fix
Fix lookup of non existent keys
2 parents df0a7e4 + 4ae8c80 commit 176acb7

File tree

2 files changed

+56
-27
lines changed

2 files changed

+56
-27
lines changed

delira/utils/config.py

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def warning_wrapper(config, key, *args, **kwargs):
1717
Parameters
1818
----------
1919
config: :class:`Config`
20-
decorated function receive :param`self` as first argument
20+
decorated function receive :param:`self` as first argument
2121
key : immutable type
2222
key which is checked
2323
@@ -134,7 +134,11 @@ def _traverse_keys(self, keys, create=False):
134134
current_level = self
135135
for k in keys:
136136
if k not in current_level:
137-
current_level[k] = self._create_internal_dict()
137+
if create:
138+
current_level[k] = self._create_internal_dict()
139+
else:
140+
raise KeyError(
141+
"{} was not found in internal dict.".format(k))
138142
# traverse to needed dict
139143
current_level = current_level[k]
140144
return current_level
@@ -249,7 +253,7 @@ def update(self, update_dict, deepcopy=False, overwrite=False):
249253
update_dict : dictlike
250254
values which should be added to config
251255
deepcopy : bool, optional
252-
copies values from :param`update_dict`, by default False
256+
copies values from :param:`update_dict`, by default False
253257
overwrite : bool, optional
254258
overwrite existing values inside config, by default False
255259
@@ -274,7 +278,7 @@ def _update(self, key, item, deepcopy=False, overwrite=False):
274278
item : Any
275279
item which should be assigned
276280
deepcopy : bool, optional
277-
copies :param`item`, by default False
281+
copies :param:`item`, by default False
278282
overwrite : bool, optional
279283
overwrite existing values inside config, by default False
280284
"""
@@ -322,9 +326,9 @@ def dump(self, path, formatter=yaml.dump, encoder_cls=Encoder, **kwargs):
322326
defines the format how the config is saved, by default yaml.dump
323327
encoder_cls : :class:`Encoder`, optional
324328
transforms config to a format which can be formatted by the
325-
:param`formatter`, by default Encoder
329+
:param:`formatter`, by default Encoder
326330
kwargs:
327-
additional keyword arguments passed to :param`formatter`
331+
additional keyword arguments passed to :param:`formatter`
328332
"""
329333
self._timestamp = now()
330334
encoded_self = encoder_cls().encode(self)
@@ -342,9 +346,9 @@ def dumps(self, formatter=yaml.dump, encoder_cls=Encoder, **kwargs):
342346
defines the format how the config is saved, by default yaml.dump
343347
encoder_cls : :class:`Encoder`, optional
344348
transforms config to a format which can be formatted by the
345-
:param`formatter`, by default Encoder
349+
:param:`formatter`, by default Encoder
346350
kwargs:
347-
additional keyword arguments passed to :param`formatter`
351+
additional keyword arguments passed to :param:`formatter`
348352
"""
349353
self._timestamp = now()
350354
encoded_self = encoder_cls().encode(self)
@@ -362,9 +366,9 @@ def load(self, path, formatter=yaml.load, decoder_cls=Decoder, **kwargs):
362366
defines the format how the config is saved, by default yaml.dump
363367
decoder_cls : :class:`Encoder`, optional
364368
transforms config to a format which can be formatted by the
365-
:param`formatter`, by default Encoder
369+
:param:`formatter`, by default Encoder
366370
kwargs:
367-
additional keyword arguments passed to :param`formatter`
371+
additional keyword arguments passed to :param:`formatter`
368372
"""
369373
with open(path, "r") as f:
370374
decoded_format = formatter(f, **kwargs)
@@ -383,9 +387,9 @@ def loads(self, data, formatter=yaml.load, decoder_cls=Decoder, **kwargs):
383387
defines the format how the config is saved, by default yaml.dump
384388
decoder_cls : :class:`Encoder`, optional
385389
transforms config to a format which can be formatted by the
386-
:param`formatter`, by default Encoder
390+
:param:`formatter`, by default Encoder
387391
kwargs:
388-
additional keyword arguments passed to :param`formatter`
392+
additional keyword arguments passed to :param:`formatter`
389393
"""
390394
decoded_format = formatter(data, **kwargs)
391395
decoded_format = decoder_cls().decode(decoded_format)
@@ -411,7 +415,7 @@ def create_from_dict(cls, value, deepcopy=False):
411415
Raises
412416
------
413417
TypeError
414-
raised if :param`value` is not a dict (or a subclass of dict)
418+
raised if :param:`value` is not a dict (or a subclass of dict)
415419
"""
416420
if not isinstance(value, dict):
417421
raise TypeError("Value must be an instance of dict but type {} "
@@ -467,9 +471,9 @@ def create_from_file(cls, path, formatter=yaml.load, decoder_cls=Decoder,
467471
defines the format how the config is saved, by default yaml.dump
468472
decoder_cls : :class:`Encoder`, optional
469473
trasforms config to a format which can be formatted by the
470-
:param`formatter`, by default Encoder
474+
:param:`formatter`, by default Encoder
471475
kwargs:
472-
additional keyword arguments passed to :param`formatter`
476+
additional keyword arguments passed to :param:`formatter`
473477
474478
Returns
475479
-------
@@ -495,9 +499,9 @@ def create_from_str(cls, data, formatter=yaml.load, decoder_cls=Decoder,
495499
defines the format how the config is saved, by default yaml.dump
496500
decoder_cls : :class:`Encoder`, optional
497501
trasforms config to a format which can be formatted by the
498-
:param`formatter`, by default Encoder
502+
:param:`formatter`, by default Encoder
499503
kwargs:
500-
additional keyword arguments passed to :param`formatter`
504+
additional keyword arguments passed to :param:`formatter`
501505
502506
Returns
503507
-------
@@ -546,29 +550,31 @@ def __contains__(self, key):
546550
"""
547551
contain = True
548552
try:
549-
self.nested_get(key)
553+
self.nested_get(key, allow_multiple=True)
550554
except KeyError:
551555
contain = False
552556
return contain
553557

554-
def nested_get(self, key, *args, **kwargs):
558+
def nested_get(self, key, *args, allow_multiple=False, **kwargs):
555559
"""
556-
Returns all occurances of :param`key` in :param`self` and subdicts
560+
Returns all occurances of :param:`key` in :param:`self` and subdicts
557561
558562
Parameters
559563
----------
560564
key : str
561565
the key to search for
562566
*args :
563567
positional arguments to provide default value
568+
allow_multiple: bool
569+
allow multiple results
564570
**kwargs :
565571
keyword arguments to provide default value
566572
567573
Raises
568574
------
569575
KeyError
570-
Multiple Values are found for key
571-
(unclear which value should be returned)
576+
Multiple Values are found for key and :param:`allow_multiple` is
577+
False (unclear which value should be returned)
572578
OR
573579
No Value was found for key and no default value was given
574580
@@ -583,7 +589,10 @@ def nested_get(self, key, *args, **kwargs):
583589
return self[key]
584590
results = nested_lookup(key, self)
585591
if len(results) > 1:
586-
raise KeyError("Multiple Values found for key %s" % key)
592+
if allow_multiple:
593+
return results
594+
else:
595+
raise KeyError("Multiple Values found for key %s" % key)
587596
elif len(results) == 0:
588597
if "default" in kwargs:
589598
return kwargs["default"]
@@ -689,7 +698,7 @@ def variable_params(self, new_params: dict):
689698
Raises
690699
------
691700
TypeError
692-
raised if :param`new_params` is not a dict (or a subclass of dict)
701+
raised if :param:`new_params` is not a dict (or a subclass of dict)
693702
"""
694703
if not isinstance(new_params, dict):
695704
raise TypeError("new_params must be an instance of dict but "
@@ -727,7 +736,7 @@ def fixed_params(self, new_params: dict):
727736
Raises
728737
------
729738
TypeError
730-
raised if :param`new_params` is not a dict (or a subclass of dict)
739+
raised if :param:`new_params` is not a dict (or a subclass of dict)
731740
"""
732741
if not isinstance(new_params, dict):
733742
raise TypeError("new_params must be an instance of dict but "
@@ -764,7 +773,7 @@ def model_params(self, new_params: dict):
764773
Raises
765774
------
766775
TypeError
767-
raised if :param`new_params` is not a dict (or a subclass of dict)
776+
raised if :param:`new_params` is not a dict (or a subclass of dict)
768777
"""
769778
if not isinstance(new_params, dict):
770779
raise TypeError("new_params must be an instance of dict but "
@@ -801,7 +810,7 @@ def training_params(self, new_params: dict):
801810
Raises
802811
------
803812
TypeError
804-
raised if :param`new_params` is not a dict (or a subclass of dict)
813+
raised if :param:`new_params` is not a dict (or a subclass of dict)
805814
"""
806815
if not isinstance(new_params, dict):
807816
raise TypeError("new_params must be an instance of dict but "

tests/utils/test_config.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@ def test_config_access(self):
9494
with self.assertWarns(RuntimeWarning, msg=warning_msg):
9595
cf[5] = 10
9696

97+
@unittest.skipUnless(
98+
check_for_no_backend(),
99+
"Test should only be executed if no backend is specified")
100+
def test_config_access_with_non_existing_keys(self):
101+
cf = self.config_cls(self.example_dict)
102+
103+
with self.assertRaises(KeyError):
104+
cf["unknown_key"]
105+
106+
with self.assertRaises(KeyError):
107+
cf["shallowStr.unknown_key"]
108+
97109
@unittest.skipUnless(
98110
check_for_no_backend(),
99111
"Test should only be executed if no backend is specified")
@@ -228,6 +240,14 @@ def test_nested_lookpup(self):
228240
self.assertIsNone(cf.nested_get("nonExistingKey", None))
229241
self.assertIsNone(cf.nested_get("nonExistingKey", default=None))
230242

243+
cf["nested_duplicate.deep"] = "duplicate"
244+
with self.assertRaises(KeyError):
245+
cf.nested_get("deep")
246+
247+
multiple_val = cf.nested_get("deep", allow_multiple=True)
248+
self.assertEqual(multiple_val, [{"deepStr": "b", "deepNum": 2},
249+
"duplicate"])
250+
231251

232252
class DeliraConfigTest(LookupConfigTest):
233253
def setUp(self):

0 commit comments

Comments
 (0)