Repository: missionpinball/mpf

Issue:
Move parse_conditional_template out of players
Parsing should be done when loading the player not during runtime. Move all calls to load time.

Discussion:

Patch:
diff --git a/mpf/config_players/event_player.py b/mpf/config_players/event_player.py
index 4bcdb7223..e15c77633 100644
--- a/mpf/config_players/event_player.py
+++ b/mpf/config_players/event_player.py
@@ -28,15 +28,15 @@ class EventPlayer(FlatConfigPlayer):
             s = deepcopy(s)
             event_dict = self.machine.placeholder_manager.parse_conditional_template(event)
 
-            if event_dict['condition'] and not event_dict['condition'].evaluate(kwargs):
+            if event_dict.condition and not event_dict.condition.evaluate(kwargs):
                 continue
 
-            if event_dict['number']:
-                delay = Util.string_to_ms(event_dict['number'])
+            if event_dict.number:
+                delay = Util.string_to_ms(event_dict.number)
                 self.delay.add(callback=self._post_event, ms=delay,
-                               event=event_dict['name'], s=s, **kwargs)
+                               event=event_dict.name, s=s, **kwargs)
             else:
-                self._post_event(event_dict['name'], s, **kwargs)
+                self._post_event(event_dict.name, s, **kwargs)
 
     def _post_event(self, event, s, **kwargs):
         event_name_placeholder = TextTemplate(self.machine, event.replace("(", "{").replace(")", "}"))
diff --git a/mpf/config_players/light_player.py b/mpf/config_players/light_player.py
index 5c74cdc4b..ed846ec27 100644
--- a/mpf/config_players/light_player.py
+++ b/mpf/config_players/light_player.py
@@ -19,6 +19,7 @@ class LightPlayer(DeviceConfigPlayer):
         """Set light color based on config."""
         instance_dict = self._get_instance_dict(context)
         full_context = self._get_full_context(context)
+        start_time = kwargs.get("start_time", None)
         del kwargs
 
         for light, s in settings.items():
@@ -34,9 +35,9 @@ class LightPlayer(DeviceConfigPlayer):
                     if not light_name or light_name[0:1] == "(" and light_name[-1:] == ")":
                         continue
                     self._light_named_color(light_name, instance_dict, full_context, s['color'], s["fade"],
-                                            final_priority)
+                                            final_priority, start_time)
             else:
-                self._light_color(light, instance_dict, full_context, s['color'], s["fade"], final_priority)
+                self._light_color(light, instance_dict, full_context, s['color'], s["fade"], final_priority, start_time)
 
     def _remove(self, settings, context):
         instance_dict = self._get_instance_dict(context)
@@ -76,7 +77,7 @@ class LightPlayer(DeviceConfigPlayer):
 
     # pylint: disable-msg=too-many-arguments
     def _light_named_color(self, light_name, instance_dict,
-                           full_context, color, fade_ms, priority):
+                           full_context, color, fade_ms, priority, start_time):
         try:
             lights = [self.machine.lights[light_name]]
         except KeyError:
@@ -86,10 +87,10 @@ class LightPlayer(DeviceConfigPlayer):
             raise AssertionError("Could not find light or tag {} in {}".format(light_name, full_context))
 
         for light in lights:
-            self._light_color(light, instance_dict, full_context, color, fade_ms, priority)
+            self._light_color(light, instance_dict, full_context, color, fade_ms, priority, start_time)
 
     # pylint: disable-msg=too-many-arguments
-    def _light_color(self, light, instance_dict, full_context, color, fade_ms, priority):
+    def _light_color(self, light, instance_dict, full_context, color, fade_ms, priority, start_time):
         if isinstance(color, str) and color == "stop":
             self._light_remove(light, instance_dict, full_context, fade_ms)
             return
@@ -101,7 +102,7 @@ class LightPlayer(DeviceConfigPlayer):
                 color = color + color + color
 
             color = RGBColor(color)
-        light.color(color, key=full_context, fade_ms=fade_ms, priority=priority)
+        light.color(color, key=full_context, fade_ms=fade_ms, priority=priority, start_time=start_time)
         instance_dict[light.name] = light
 
     def clear_context(self, context):
diff --git a/mpf/config_players/score_queue_player.py b/mpf/config_players/score_queue_player.py
index d31af7825..beb681329 100644
--- a/mpf/config_players/score_queue_player.py
+++ b/mpf/config_players/score_queue_player.py
@@ -37,13 +37,13 @@ class ScoreQueuePlayer(ConfigPlayer):
             self.raise_config_error("Settings of score_queue_player should "
                                     "be a dict. But are: {}".format(settings), 5, context=name)
         for var, s in settings.items():
-            var_dict = self.machine.placeholder_manager.parse_conditional_template(var)
+            var_conditional_event = self.machine.placeholder_manager.parse_conditional_template(var)
             value_dict = self._parse_config(s, name)
-            value_dict["condition"] = var_dict["condition"]
-            config[var_dict["name"]] = value_dict
-            if not bool(re.match('^[0-9a-zA-Z_-]+$', var_dict["name"])):
+            value_dict["condition"] = var_conditional_event.condition
+            config[var_conditional_event.name] = value_dict
+            if not bool(re.match('^[0-9a-zA-Z_-]+$', var_conditional_event.name)):
                 self.raise_config_error("Variable may only contain letters, numbers, dashes and underscores. "
-                                        "Name: {}".format(var_dict["name"]), 4, context=name)
+                                        "Name: {}".format(var_conditional_event.name), 4, context=name)
         return config
 
     def get_express_config(self, value: Any) -> dict:
diff --git a/mpf/config_players/show_player.py b/mpf/config_players/show_player.py
index 18d0e00b9..8636e0d85 100644
--- a/mpf/config_players/show_player.py
+++ b/mpf/config_players/show_player.py
@@ -35,8 +35,7 @@ class ShowPlayer(DeviceConfigPlayer):
             start_time = self.machine.clock.get_time()
         for show, show_settings in settings.items():
             # Look for a conditional event in the show name
-            show_dict = self.machine.placeholder_manager.parse_conditional_template(show)
-            if show_dict['condition'] and not show_dict['condition'].evaluate(kwargs):
+            if show.condition and show.condition.evaluate(kwargs):
                 continue
 
             show_settings = dict(show_settings)
@@ -49,7 +48,14 @@ class ShowPlayer(DeviceConfigPlayer):
                 show_settings['priority'] = priority
             # todo need to add this key back to the config player
 
-            self._update_show(show_dict["name"], show_settings, context, queue, start_time, kwargs)
+            self._update_show(show.name, show_settings, context, queue, start_time, kwargs)
+
+    def _expand_device(self, device):
+        # parse conditionals
+        devices = super()._expand_device(device)
+        for index, device_entry in enumerate(devices):
+            devices[index] = self.machine.placeholder_manager.parse_conditional_template(device_entry)
+        return devices
 
     def handle_subscription_change(self, value, settings, priority, context):
         """Handle subscriptions."""
@@ -62,12 +68,12 @@ class ShowPlayer(DeviceConfigPlayer):
             if 'key' in show_settings and show_settings['key']:
                 key = show_settings['key']
             else:
-                key = show
+                key = show.name
 
             if value:
-                self._play(key, instance_dict, show, show_settings, False, None, {})
+                self._play(key, instance_dict, show.name, show_settings, False, None, {})
             else:
-                self._stop(key, instance_dict, show, show_settings, False, None, {})
+                self._stop(key, instance_dict, show.name, show_settings, False, None, {})
 
     # pylint: disable-msg=too-many-arguments
     def _play(self, key, instance_dict, show, show_settings, queue, start_time, placeholder_args):
diff --git a/mpf/config_players/variable_player.py b/mpf/config_players/variable_player.py
index 36c1e9036..992d7cdd5 100644
--- a/mpf/config_players/variable_player.py
+++ b/mpf/config_players/variable_player.py
@@ -116,13 +116,13 @@ class VariablePlayer(ConfigPlayer):
             self.raise_config_error("Settings of variable_player should "
                                     "be a dict. But are: {}".format(settings), 5, context=name)
         for var, s in settings.items():
-            var_dict = self.machine.placeholder_manager.parse_conditional_template(var)
+            var_conditional_event = self.machine.placeholder_manager.parse_conditional_template(var)
             value_dict = self._parse_config(s, name)
-            value_dict["condition"] = var_dict["condition"]
-            config[var_dict["name"]] = value_dict
-            if not bool(re.match('^[0-9a-zA-Z_-]+$', var_dict["name"])):
+            value_dict["condition"] = var_conditional_event.condition
+            config[var_conditional_event.name] = value_dict
+            if not bool(re.match('^[0-9a-zA-Z_-]+$', var_conditional_event.name)):
                 self.raise_config_error("Variable may only contain letters, numbers, dashes and underscores. "
-                                        "Name: {}".format(var_dict["name"]), 4, context=name)
+                                        "Name: {}".format(var_conditional_event.name), 4, context=name)
         return config
 
     def get_express_config(self, value: Any) -> dict:
diff --git a/mpf/core/assets.py b/mpf/core/assets.py
index a48bbf5f3..02700d676 100644
--- a/mpf/core/assets.py
+++ b/mpf/core/assets.py
@@ -661,18 +661,18 @@ class AssetPool:
             config['type'] = 'sequence'
 
         for asset in Util.string_to_list(self.config[self.member_cls.config_section]):
-            asset_dict = self.machine.placeholder_manager.parse_conditional_template(asset, default_number=1)
+            asset_condition = self.machine.placeholder_manager.parse_conditional_template(asset, default_number=1)
 
             # For efficiency, track whether any assets have conditions
-            if asset_dict['condition']:
+            if asset_condition.condition:
                 self._has_conditions = True
 
             try:
                 self.assets.append((
-                    getattr(self.machine, self.member_cls.attribute)[asset_dict['name']],
-                    asset_dict['number'], asset_dict['condition']))
+                    getattr(self.machine, self.member_cls.attribute)[asset_condition.name],
+                    asset_condition.number, asset_condition.condition))
             except KeyError:    # pragma: no cover
-                raise ValueError("No asset named {}".format(asset_dict['name']))
+                raise ValueError("No asset named {}".format(asset_condition.name))
 
         self._configure_return_asset()
 
diff --git a/mpf/core/bcp/bcp_client.py b/mpf/core/bcp/bcp_client.py
index af1b78972..009b6fcda 100644
--- a/mpf/core/bcp/bcp_client.py
+++ b/mpf/core/bcp/bcp_client.py
@@ -10,6 +10,8 @@ class BaseBcpClient(MpfController, metaclass=abc.ABCMeta):
 
     """Base class for bcp clients."""
 
+    __slots__ = ["name", "bcp", "exit_on_close"]
+
     def __init__(self, machine, name, bcp):
         """Initialise client."""
         super().__init__(machine)
diff --git a/mpf/core/bcp/bcp_socket_client.py b/mpf/core/bcp/bcp_socket_client.py
index d8aa5f6de..a792c33eb 100644
--- a/mpf/core/bcp/bcp_socket_client.py
+++ b/mpf/core/bcp/bcp_socket_client.py
@@ -200,11 +200,12 @@ class BCPClientSocket(BaseBcpClient):
         bcp: The bcp object.
     """
 
+    config_name = 'bcp_client'
+
+    __slots__ = ["_sender", "_receiver", "_send_goodbye", "_receive_buffer", "_bcp_client_socket_commands", "__dict__"]
+
     def __init__(self, machine, name, bcp):
         """Initialise BCP client socket."""
-        self.module_name = 'BCPClientSocket.{}'.format(name)
-        self.config_name = 'bcp_client'
-
         super().__init__(machine, name, bcp)
 
         self._sender = None
@@ -217,7 +218,7 @@ class BCPClientSocket(BaseBcpClient):
 
     def __repr__(self):
         """Return str representation."""
-        return self.module_name
+        return 'BCPClientSocket.{}'.format(self.name)
 
     def connect(self, config):
         """Actively connect to server."""
diff --git a/mpf/core/mode_controller.py b/mpf/core/mode_controller.py
index e3376fd01..a815faf9f 100644
--- a/mpf/core/mode_controller.py
+++ b/mpf/core/mode_controller.py
@@ -156,7 +156,7 @@ class ModeController(MpfController):
                 self.machine.config['mpf']['paths']['modes'],
                 self._machine_mode_folders[mode_string],
                 'config',
-                os.path.basename(self._machine_mode_folders[mode_string]) + '.yaml')
+                self._machine_mode_folders[mode_string] + '.yaml')
             if not os.path.isfile(mode_config_file):
                 return False
         except KeyError:
@@ -310,29 +310,10 @@ class ModeController(MpfController):
                 folder)
 
             if os.path.isdir(this_mode_folder) and not folder.startswith('_'):
-                if 'config' not in os.listdir(this_mode_folder):
-                    sub_folders = self._get_sub_folders(base_folder, this_mode_folder)
-                    final_mode_folders.update(sub_folders)
-                else:
-                    final_mode_folders[folder] = folder
+                final_mode_folders[folder] = folder
 
         return final_mode_folders
 
-    def _get_sub_folders(self, root_folder, base_folder):
-        final_sub_folders = dict()
-
-        for folder in os.listdir(base_folder):
-            this_mode_folder = os.path.join(base_folder, folder)
-
-            if os.path.isdir(this_mode_folder) and not folder.startswith('_'):
-                if 'config' not in os.listdir(this_mode_folder):
-                    final_sub_folders.update(self._get_sub_folders(root_folder, this_mode_folder))
-                else:
-                    final_sub_folders[folder] = os.path.relpath(this_mode_folder, os.path.join(
-                        root_folder, self.machine.config['mpf']['paths']['modes']))
-
-        return final_sub_folders
-
     @classmethod
     def _player_added(cls, player, num, **kwargs):
         del num
diff --git a/mpf/core/placeholder_manager.py b/mpf/core/placeholder_manager.py
index 359b913e5..f049561ee 100644
--- a/mpf/core/placeholder_manager.py
+++ b/mpf/core/placeholder_manager.py
@@ -28,6 +28,23 @@ bool_operators = {ast.And: lambda a, b: a and b, ast.Or: lambda a, b: a or b}
 comparisons = {ast.Eq: op.eq, ast.Lt: op.lt, ast.Gt: op.gt, ast.LtE: op.le, ast.GtE: op.ge, ast.NotEq: op.ne}
 
 
+class ConditionalEvent:
+
+    """An conditional event."""
+
+    __slots__ = ["name", "condition", "number"]
+
+    def __init__(self, name, condition, number):
+        """Initialise conditional event."""
+        self.name = name
+        self.condition = condition
+        self.number = number
+
+    def __repr__(self):
+        """Return string representation."""
+        return "{}-{}-{}".format(self.name, self.condition, self.number)
+
+
 class TemplateEvalError(Exception):
 
     """An error occurred during a template evaluation."""
@@ -794,7 +811,7 @@ class BasePlaceholderManager(MpfController):
                     self.warning_log("Condition '{}' has invalid number value '{}'".format(
                                      template, match_dict['number']))
                     match_dict['number'] = default_number
-        return match_dict
+        return ConditionalEvent(match_dict["name"], match_dict["condition"], match_dict["number"])
 
 
 class PlaceholderManager(BasePlaceholderManager):
diff --git a/mpf/core/rgb_color.py b/mpf/core/rgb_color.py
index 62f1a1eb8..955bec659 100644
--- a/mpf/core/rgb_color.py
+++ b/mpf/core/rgb_color.py
@@ -254,46 +254,26 @@ class RGBColor:
         """Return the red component of the RGB color representation."""
         return self._color[0]
 
-    @red.setter
-    def red(self, value: int):
-        self._color = (value, self._color[1], self._color[2])
-
     @property
     def green(self) -> int:
         """Return the green component of the RGB color representation."""
         return self._color[1]
 
-    @green.setter
-    def green(self, value: int):
-        self._color = (self._color[0], value, self._color[2])
-
     @property
     def blue(self) -> int:
         """Return the blue component of the RGB color representation."""
         return self._color[2]
 
-    @blue.setter
-    def blue(self, value: int):
-        self._color = (self._color[0], self._color[1], value)
-
     @property
     def rgb(self) -> Tuple[int, int, int]:
         """Return an RGB representation of the color."""
         return self._color
 
-    @rgb.setter
-    def rgb(self, value: Tuple[int, int, int]):
-        self._color = value
-
     @property
     def hex(self) -> str:
         """Return a 6-char HEX representation of the color."""
         return RGBColor.rgb_to_hex(self.rgb)
 
-    @hex.setter
-    def hex(self, value: str):
-        self._color = RGBColor.hex_to_rgb(value)
-
     @property
     def name(self) -> str:
         """Return the color name or None.
@@ -306,10 +286,6 @@ class RGBColor:
             [(_v, _k) for _k, _v in list(named_rgb_colors.items())]).get(
             self._color)
 
-    @name.setter
-    def name(self, value: str):
-        self._color = RGBColor.name_to_rgb(value)
-
     @staticmethod
     def rgb_to_hex(rgb: Tuple[int, int, int]) -> str:
         """Convert an RGB color representation to a HEX color representation.
@@ -424,7 +400,7 @@ class RGBColor:
             # instead we just add the upper case string to the color hash map so next time we will hit the fast path
             rgb = named_rgb_colors.get(value.lower())
             if rgb:
-                named_rgb_colors[value] = rgb
+                named_rgb_colors[value.lower()] = rgb
         if rgb is None:
             rgb = RGBColor.hex_to_rgb(value)
             if rgb is None:
diff --git a/mpf/devices/light.py b/mpf/devices/light.py
index bfda79bcd..6ca69f5c7 100644
--- a/mpf/devices/light.py
+++ b/mpf/devices/light.py
@@ -50,7 +50,7 @@ class Light(SystemWideDevice, DevicePositionMixin):
     class_label = 'light'
 
     __slots__ = ["hw_drivers", "platforms", "delay", "default_fade_ms", "_color_correction_profile", "stack",
-                 "hw_driver_functions"]
+                 "hw_driver_functions", "_off_color"]
 
     def __init__(self, machine, name):
         """Initialise light."""
@@ -62,6 +62,7 @@ class Light(SystemWideDevice, DevicePositionMixin):
         self.delay = DelayManager(self.machine)
 
         self.default_fade_ms = None
+        self._off_color = RGBColor("off")
 
         self._color_correction_profile = None
 
@@ -276,7 +277,8 @@ class Light(SystemWideDevice, DevicePositionMixin):
         """
         self._color_correction_profile = profile
 
-    def color(self, color, fade_ms=None, priority=0, key=None):
+    # pylint: disable-msg=too-many-arguments
+    def color(self, color, fade_ms=None, priority=0, key=None, start_time=None):
         """Add or update a color entry in this light's stack.
 
         Calling this methods is how you tell this light what color you want it to be.
@@ -311,7 +313,8 @@ class Light(SystemWideDevice, DevicePositionMixin):
         if fade_ms is None:
             fade_ms = self.default_fade_ms
 
-        start_time = self.machine.clock.get_time()
+        if not start_time:
+            start_time = self.machine.clock.get_time()
 
         color_changes = not self.stack or self.stack[0].priority <= priority or self.stack[0].dest_color is None
 
@@ -344,7 +347,7 @@ class Light(SystemWideDevice, DevicePositionMixin):
             fade_ms: duration of fade
         """
         del kwargs
-        self.color(color=RGBColor(), fade_ms=fade_ms, priority=priority,
+        self.color(color=self._off_color, fade_ms=fade_ms, priority=priority,
                    key=key)
 
     # pylint: disable-msg=too-many-arguments
@@ -356,14 +359,14 @@ class Light(SystemWideDevice, DevicePositionMixin):
         elif not isinstance(key, str):
             raise AssertionError("Key should be string")
 
-        if priority < self._get_priority_from_key(key):
+        if self.stack and priority < self._get_priority_from_key(key):
             if self._debug:
                 self.debug_log("Incoming priority %s is lower than an existing "
                                "stack item with the same key %s. Not adding to "
                                "stack.", priority, key)
             return
 
-        if self._debug and self.stack and priority == self.stack[0].priority and key != self.stack[0].key:
+        if self.stack and self._debug and priority == self.stack[0].priority and key != self.stack[0].key:
             self.debug_log("Light stack contains two entries with the same priority %s but different keys: %s",
                            priority, self.stack)
 
@@ -374,7 +377,8 @@ class Light(SystemWideDevice, DevicePositionMixin):
             dest_time = 0
             color_below = None
 
-        self._remove_from_stack_by_key(key)
+        if self.stack:
+            self._remove_from_stack_by_key(key)
 
         self.stack.append(LightStackEntry(priority,
                                           key,
@@ -567,7 +571,7 @@ class Light(SystemWideDevice, DevicePositionMixin):
             color_settings = stack[0]
         except IndexError:
             # no stack
-            return RGBColor('off'), -1, True
+            return self._off_color, -1, True
 
         dest_color = color_settings.dest_color
 
@@ -632,7 +636,7 @@ class Light(SystemWideDevice, DevicePositionMixin):
         """
         if not self.stack:
             # no stack -> we are black
-            return RGBColor("off")
+            return self._off_color
 
         if self.stack[0].key == key and self.stack[0].priority == priority:
             # fast path for resetting the top element
diff --git a/mpf/modes/carousel/code/carousel.py b/mpf/modes/carousel/code/carousel.py
index 38f1ca449..40cc9b093 100644
--- a/mpf/modes/carousel/code/carousel.py
+++ b/mpf/modes/carousel/code/carousel.py
@@ -30,7 +30,7 @@ class Carousel(Mode):
         for item in Util.string_to_list(mode_settings.get("selectable_items", "")):
             placeholder = self.machine.placeholder_manager.parse_conditional_template(item)
             # Only add a placeholder if there's a condition, otherwise just the string
-            self._all_items.append(placeholder if placeholder['condition'] else item)
+            self._all_items.append(placeholder if placeholder.condition else item)
         self._select_item_events = Util.string_to_list(mode_settings.get("select_item_events", ""))
         self._next_item_events = Util.string_to_list(mode_settings.get("next_item_events", ""))
         self._previous_item_events = Util.string_to_list(mode_settings.get("previous_item_events", ""))
@@ -46,8 +46,8 @@ class Carousel(Mode):
             # All strings go in, but only conditional templates if they evaluate true
             if isinstance(item, str):
                 self._items.append(item)
-            elif not item['condition'] or item['condition'].evaluate({}):
-                self._items.append(item['name'])
+            elif not item.condition or item.condition.evaluate({}):
+                self._items.append(item.name)
         if not self._items:
             self.machine.events.post("{}_items_empty".format(self.name))
             '''event (carousel_name)_items_empty


Test patch:
diff --git a/mpf/tests/machine_files/mode_tests/config/test_modes.yaml b/mpf/tests/machine_files/mode_tests/config/test_modes.yaml
index 2e4dad92a..7fe7878fb 100644
--- a/mpf/tests/machine_files/mode_tests/config/test_modes.yaml
+++ b/mpf/tests/machine_files/mode_tests/config/test_modes.yaml
@@ -5,6 +5,3 @@ modes:
   - mode2
   - mode3
   - mode4
-  - mode5
-  - mode6
-  - mode7
\ No newline at end of file
diff --git a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/mode5/config/mode5.yaml b/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/mode5/config/mode5.yaml
deleted file mode 100644
index 029fa2d15..000000000
--- a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/mode5/config/mode5.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-#config_version=5
-mode:
-  start_events: start_mode5
-  stop_events: stop_mode5
-  priority: 200
-  start_priority: 1
-  stop_on_ball_end: false
-  game_mode: False
-
-mode_settings:
-  this: true
-
-config:
-  - test.yaml
diff --git a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/mode5/config/test.yaml b/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/mode5/config/test.yaml
deleted file mode 100644
index 9185171d8..000000000
--- a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/mode5/config/test.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-#config_version=5
-mode_settings:
-  test: 123
\ No newline at end of file
diff --git a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/mode5/mode8/config/mode8.yaml b/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/mode5/mode8/config/mode8.yaml
deleted file mode 100644
index 6bc335992..000000000
--- a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/mode5/mode8/config/mode8.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-#config_version=5
-mode:
-  start_events: start_mode8
-  stop_events: stop_mode8
-  priority: 200
-  start_priority: 1
-  stop_on_ball_end: false
-  game_mode: False
-
-mode_settings:
-  this: true
-
-config:
-  - test.yaml
diff --git a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/mode5/mode8/config/test.yaml b/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/mode5/mode8/config/test.yaml
deleted file mode 100644
index 9185171d8..000000000
--- a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/mode5/mode8/config/test.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-#config_version=5
-mode_settings:
-  test: 123
\ No newline at end of file
diff --git a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/sub_mode/mode6/config/mode6.yaml b/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/sub_mode/mode6/config/mode6.yaml
deleted file mode 100644
index 8d09ecf6d..000000000
--- a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/sub_mode/mode6/config/mode6.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-#config_version=5
-mode:
-  start_events: start_mode6
-  stop_events: stop_mode6
-  priority: 200
-  start_priority: 1
-  stop_on_ball_end: false
-  game_mode: False
-
-mode_settings:
-  this: true
-
-config:
-  - test.yaml
diff --git a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/sub_mode/mode6/config/test.yaml b/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/sub_mode/mode6/config/test.yaml
deleted file mode 100644
index 9185171d8..000000000
--- a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/sub_mode/mode6/config/test.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-#config_version=5
-mode_settings:
-  test: 123
\ No newline at end of file
diff --git a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/sub_mode/mode7/config/mode7.yaml b/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/sub_mode/mode7/config/mode7.yaml
deleted file mode 100644
index 2913df0c7..000000000
--- a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/sub_mode/mode7/config/mode7.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-#config_version=5
-mode:
-  start_events: start_mode7
-  stop_events: stop_mode7
-  priority: 200
-  start_priority: 1
-  stop_on_ball_end: false
-  game_mode: False
-
-mode_settings:
-  this: true
-
-config:
-  - test.yaml
diff --git a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/sub_mode/mode7/config/test.yaml b/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/sub_mode/mode7/config/test.yaml
deleted file mode 100644
index 9185171d8..000000000
--- a/mpf/tests/machine_files/mode_tests/modes/mode_in_sub_folder/sub_mode/mode7/config/test.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-#config_version=5
-mode_settings:
-  test: 123
\ No newline at end of file
diff --git a/mpf/tests/test_Modes.py b/mpf/tests/test_Modes.py
index c85016ddb..fef7fe5eb 100644
--- a/mpf/tests/test_Modes.py
+++ b/mpf/tests/test_Modes.py
@@ -19,10 +19,6 @@ class TestModes(MpfFakeGameTestCase):
         self.assertIn('mode2', self.machine.modes)
         self.assertIn('mode3', self.machine.modes)
         self.assertIn('mode4', self.machine.modes)
-        self.assertIn('mode5', self.machine.modes)
-        self.assertIn('mode6', self.machine.modes)
-        self.assertIn('mode7', self.machine.modes)
-        self.assertNotIn('mode8', self.machine.modes)
 
     def test_mode_start_stop(self):
         # Setup mocked event handlers for mode start/stop sequence
diff --git a/mpf/tests/test_PlaceholderManager.py b/mpf/tests/test_PlaceholderManager.py
index ac4fd7dac..ccbb2bf70 100644
--- a/mpf/tests/test_PlaceholderManager.py
+++ b/mpf/tests/test_PlaceholderManager.py
@@ -23,48 +23,48 @@ class TestPlaceholderManager(unittest.TestCase):
 
         # test no conditions or numbers
         d = p.parse_conditional_template("test_string")
-        self.assertEqual(d["name"], "test_string")
-        self.assertIsNone(d["condition"])
-        self.assertIsNone(d["number"])
+        self.assertEqual(d.name, "test_string")
+        self.assertIsNone(d.condition)
+        self.assertIsNone(d.number)
         # test number with pipe
         d = p.parse_conditional_template("test_string|12")
-        self.assertEqual(d["name"], "test_string")
-        self.assertIsNone(d["condition"])
-        self.assertEqual(d["number"], "12")
+        self.assertEqual(d.name, "test_string")
+        self.assertIsNone(d.condition)
+        self.assertEqual(d.number, "12")
         # test number with colon
         d = p.parse_conditional_template("test_string:21")
-        self.assertEqual(d["name"], "test_string")
-        self.assertIsNone(d["condition"])
-        self.assertEqual(d["number"], "21")
+        self.assertEqual(d.name, "test_string")
+        self.assertIsNone(d.condition)
+        self.assertEqual(d.number, "21")
         # test conditional event
         d = p.parse_conditional_template("test_string{somecondition<1}")
-        self.assertEqual(d["name"], "test_string")
-        self.assertIsInstance(d["condition"], BoolTemplate)
-        self.assertIsNone(d["number"])
+        self.assertEqual(d.name, "test_string")
+        self.assertIsInstance(d.condition, BoolTemplate)
+        self.assertIsNone(d.number)
         # test conditional event with pipe
         d = p.parse_conditional_template("test_string{somecondition<1}|32")
-        self.assertEqual(d["name"], "test_string")
-        self.assertIsInstance(d["condition"], BoolTemplate)
-        self.assertEqual(d["number"], "32")
+        self.assertEqual(d.name, "test_string")
+        self.assertIsInstance(d.condition, BoolTemplate)
+        self.assertEqual(d.number, "32")
         # test conditional event with colon
         d = p.parse_conditional_template("test_string{somecondition<1}:34")
-        self.assertEqual(d["name"], "test_string")
-        self.assertIsInstance(d["condition"], BoolTemplate)
-        self.assertEqual(d["number"], "34")
+        self.assertEqual(d.name, "test_string")
+        self.assertIsInstance(d.condition, BoolTemplate)
+        self.assertEqual(d.number, "34")
         # test default number
         d = p.parse_conditional_template("test_string", default_number=2)
-        self.assertEqual(d["name"], "test_string")
-        self.assertEqual(d["number"], 2)
+        self.assertEqual(d.name, "test_string")
+        self.assertEqual(d.number, 2)
         # test number typing
         d = p.parse_conditional_template("test_string|45", default_number=1.0)
-        self.assertEqual(d["number"], 45)
-        self.assertIsInstance(d["number"], float)
+        self.assertEqual(d.number, 45)
+        self.assertIsInstance(d.number, float)
         # test number without typing
         d = p.parse_conditional_template("test_string:500ms")
-        self.assertEqual(d["number"], "500ms")
+        self.assertEqual(d.number, "500ms")
         # test fallback on unparseable number
         d = p.parse_conditional_template("test_string|foobar", default_number=8)
-        self.assertEqual(d["number"], 8)
+        self.assertEqual(d.number, 8)
 
 class TestPlaceholderManagerWithMachine(MpfFakeGameTestCase):
 
diff --git a/mpf/tests/test_RGBColor.py b/mpf/tests/test_RGBColor.py
index 1660e4ecf..730f132ea 100644
--- a/mpf/tests/test_RGBColor.py
+++ b/mpf/tests/test_RGBColor.py
@@ -32,8 +32,7 @@ class TestRGBColor(unittest.TestCase):
 
     def test_off_color(self):
         # Tests the 'Off' color (nicely readable in LED show files)
-        color = RGBColor()
-        color.name = 'Off'
+        color = RGBColor('Off')
         self.assertEqual((0, 0, 0), color.rgb)
         self.assertIn(color.name, ['black', 'off'])
 
@@ -57,8 +56,7 @@ class TestRGBColor(unittest.TestCase):
         self.assertEqual((240, 248, 255), RGBColor((240, 248, 255)).rgb)
 
     def test_properties(self):
-        color1 = RGBColor()
-        color1.name = 'DarkSlateBlue'
+        color1 = RGBColor('DarkSlateBlue')
         self.assertEqual((72, 61, 139), color1.rgb)
         self.assertEqual(72, color1.red)
         self.assertEqual(61, color1.green)
@@ -66,8 +64,7 @@ class TestRGBColor(unittest.TestCase):
         self.assertEqual('darkslateblue', color1.name)
         self.assertEqual('483d8b', color1.hex)
 
-        color2 = RGBColor()
-        color2.rgb = (130, 130, 130)
+        color2 = RGBColor((130, 130, 130))
 
         color_sum = color1 + color2
         self.assertEqual((202, 191, 255), color_sum.rgb)