Repository: speechbrain/speechbrain

Issue:
Throw error when override does not match contents of yaml file
Currently, it is hard to know if overrides work correctly because it fails silently when the override does not match what is in a yaml file. We could potentially also add a flag that would turn off this error or convert it to a warning.


Discussion:

Resolving patch:
diff --git a/speechbrain/utils/data_utils.py b/speechbrain/utils/data_utils.py
--- a/speechbrain/utils/data_utils.py
+++ b/speechbrain/utils/data_utils.py
@@ -168,7 +168,7 @@ def recursive_items(dictionary):
             yield (key, value)
 
 
-def recursive_update(d, u):
+def recursive_update(d, u, must_match=False):
     """Similar function to `dict.update`, but for a nested `dict`.
 
     From: https://stackoverflow.com/a/3233356
@@ -195,6 +195,8 @@ def recursive_update(d, u):
         mapping to be updated
     u : dict
         mapping to update with
+    must_match : bool
+        Whether to throw an error if the key in `u` does not exist in `d`.
 
     Example
     -------
@@ -208,6 +210,10 @@ def recursive_update(d, u):
     for k, v in u.items():
         if isinstance(v, collections.abc.Mapping) and k in d:
             recursive_update(d.get(k, {}), v)
+        elif must_match and k not in d:
+            raise KeyError(
+                f"Override '{k}' not found in: {[key for key in d.keys()]}"
+            )
         else:
             d[k] = v
 
diff --git a/speechbrain/yaml.py b/speechbrain/yaml.py
--- a/speechbrain/yaml.py
+++ b/speechbrain/yaml.py
@@ -14,7 +14,7 @@
 
 # NOTE: Empty dict as default parameter is fine here since overrides are never
 # modified
-def load_extended_yaml(yaml_stream, overrides={}):
+def load_extended_yaml(yaml_stream, overrides={}, overrides_must_match=True):
     r'''This function implements the SpeechBrain extended YAML syntax
 
     The purpose for this syntax is a compact, structured hyperparameter and
@@ -60,6 +60,9 @@ def load_extended_yaml(yaml_stream, overrides={}):
         A set of overrides for the values read from the stream.
         As yaml implements a nested structure, so can the overrides.
         See `speechbrain.utils.data_utils.recursive_update`
+    overrides_must_match : bool
+        Whether an error will be thrown when an override does not match
+        a corresponding key in the yaml_stream.
 
     Returns
     -------
@@ -76,12 +79,14 @@ def load_extended_yaml(yaml_stream, overrides={}):
     >>> params.thing
     Counter({'b': 3})
     '''
-    yaml_stream = resolve_references(yaml_stream, overrides)
+    yaml_stream = resolve_references(
+        yaml_stream, overrides, overrides_must_match
+    )
     yaml.Loader.add_multi_constructor("!", object_constructor)
     return SimpleNamespace(**yaml.load(yaml_stream, Loader=yaml.Loader))
 
 
-def resolve_references(yaml_stream, overrides={}):
+def resolve_references(yaml_stream, overrides={}, overrides_must_match=False):
     r'''Resolves inter-document references, a component of extended YAML.
 
     Arguments
@@ -91,6 +96,11 @@ def resolve_references(yaml_stream, overrides={}):
         written with the extended YAML syntax.
     overrides : mapping
         A set of keys for which to change the value listed in the stream.
+    overrides_must_match : bool
+        Whether an error will be thrown when an override does not match
+        a corresponding key in the yaml_stream. This is the opposite
+        default from `load_extended_yaml` because `resolve_references`
+        doesn't need to be as strict by default.
 
     Returns
     -------
@@ -111,7 +121,7 @@ def resolve_references(yaml_stream, overrides={}):
     # using ruamel.yaml to preserve the tags
     ruamel_yaml = ruamel.yaml.YAML()
     preview = ruamel_yaml.load(yaml_stream)
-    recursive_update(preview, overrides)
+    recursive_update(preview, overrides, must_match=overrides_must_match)
     _walk_tree_and_resolve(current_node=preview, tree=preview)
 
     # Dump back to string so we can load with bells and whistles


Test patch: