Repository: missionpinball/mpf
Issue: Catch duplicate sections in config 

The second one currently silently overwrites the first one. Raise an error instead 


Changed files:
mpf/file_interfaces/yaml_interface.py, mpf/tests/test_YamlInterface.py

diff --git a/mpf/file_interfaces/yaml_interface.py b/mpf/file_interfaces/yaml_interface.py
index c91a718c1..bd5888178 100644
--- a/mpf/file_interfaces/yaml_interface.py
+++ b/mpf/file_interfaces/yaml_interface.py
@@ -7,13 +7,14 @@ import copy
 import logging
 import re
 
+import collections
 import ruamel.yaml as yaml
 from ruamel.yaml.reader import Reader
 from ruamel.yaml.resolver import BaseResolver, Resolver
 from ruamel.yaml.scanner import Scanner, RoundTripScanner
 from ruamel.yaml.parser_ import Parser
 from ruamel.yaml.composer import Composer
-from ruamel.yaml.constructor import Constructor, RoundTripConstructor
+from ruamel.yaml.constructor import Constructor, RoundTripConstructor, ConstructorError
 from ruamel.yaml.compat import to_str
 from ruamel.yaml.dumper import RoundTripDumper
 
@@ -157,6 +158,36 @@ class MpfConstructor(Constructor):
         """Initialise."""
         super().__init__()
 
+    def construct_mapping(self, node, deep=False):
+        """Construct mapping but raise error when a section is defined twice.
+
+        From: http://stackoverflow.com/questions/34358974/how-to-prevent-re-definition-of-keys-in-yaml
+        """
+        if not isinstance(node, yaml.MappingNode):
+            raise ConstructorError(
+                None, None,
+                "expected a mapping node, but found %s" % node.id,
+                node.start_mark)
+        mapping = {}
+        for key_node, value_node in node.value:
+            # keys can be list -> deep
+            key = self.construct_object(key_node, deep=True)
+            # lists are not hashable, but tuples are
+            if not isinstance(key, collections.Hashable):
+                if isinstance(key, list):
+                    key = tuple(key)
+            if not isinstance(key, collections.Hashable):
+                raise ConstructorError(
+                    "while constructing a mapping", node.start_mark,
+                    "found unhashable key", key_node.start_mark)
+
+            value = self.construct_object(value_node, deep=deep)
+            # next two lines differ from original
+            if key in mapping:
+                raise KeyError("Key \\"{}\\" was defined multiple times in config.".format(key))
+            mapping[key] = value
+        return mapping
+
     def construct_yaml_int(self, node):
         """Add int fix."""
         value = to_str(self.construct_scalar(node))
diff --git a/mpf/tests/test_YamlInterface.py b/mpf/tests/test_YamlInterface.py
index c3662e762..c8d29068e 100644
--- a/mpf/tests/test_YamlInterface.py
+++ b/mpf/tests/test_YamlInterface.py
@@ -1,7 +1,7 @@
 import unittest
 import ruamel.yaml as yaml
 from ruamel.yaml.loader import RoundTripLoader
-from mpf.file_interfaces.yaml_interface import YamlInterface
+from mpf.file_interfaces.yaml_interface import YamlInterface, MpfLoader
 
 
 class TestYamlInterface(unittest.TestCase):
@@ -78,18 +78,16 @@ sound_system:
 
         self.assertEqual(orig_config, saved_config)
 
-    def test_rename_key(self):
+    def test_duplicate_key(self):
         yaml_str = '''
 
 a: 1
 b: 2
-c: 3
+a: 3
 
 
 '''
-        data = yaml.load(yaml_str, Loader=RoundTripLoader)
-        YamlInterface.rename_key('b', 'z', data)
+        with self.assertRaises(KeyError):
+            yaml.load(yaml_str, Loader=MpfLoader)
+
 
-        self.assertEqual(data['a'], 1)
-        self.assertEqual(data['z'], 2)
-        self.assertEqual(data['c'], 3)