From f0ec51711c85f94753577ffaeb79cf849da06cdc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 Sep 2016 22:25:01 -0700 Subject: [PATCH] Bugfix group order (#3323) * Add ordered dict config validator * Have group component use ordered dict config validator * Improve config_validation testing * update doc string config_validation.ordered_dict * validate full dict entries * Further simplify ordered_dict validator. * Lint fix --- homeassistant/components/group.py | 4 +- homeassistant/helpers/config_validation.py | 22 ++++++++++ tests/components/test_group.py | 17 +++++--- tests/helpers/test_config_validation.py | 50 ++++++++++++++++++++++ 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index c4cd177925d..41901d87e86 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -46,12 +46,12 @@ def _conf_preprocess(value): CONFIG_SCHEMA = vol.Schema({ - DOMAIN: {cv.match_all: vol.Schema(vol.All(_conf_preprocess, { + DOMAIN: cv.ordered_dict(vol.All(_conf_preprocess, { vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None), CONF_VIEW: cv.boolean, CONF_NAME: cv.string, CONF_ICON: cv.icon, - }))} + }, cv.match_all)) }, extra=vol.ALLOW_EXTRA) # List of ON/OFF state tuples for groupable states diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 1be157c789d..009736024a1 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1,4 +1,5 @@ """Helpers for config validation using voluptuous.""" +from collections import OrderedDict from datetime import timedelta import os from urllib.parse import urlparse @@ -290,6 +291,27 @@ def url(value: Any) -> str: raise vol.Invalid('invalid url') +def ordered_dict(value_validator, key_validator=match_all): + """Validate an ordered dict validator that maintains ordering. + + value_validator will be applied to each value of the dictionary. + key_validator (optional) will be applied to each key of the dictionary. + """ + item_validator = vol.Schema({key_validator: value_validator}) + + def validator(value): + """Validate ordered dict.""" + config = OrderedDict() + + for key, val in value.items(): + v_res = item_validator({key: val}) + config.update(v_res) + + return config + + return validator + + # Validator helpers def key_dependency(key, dependency): diff --git a/tests/components/test_group.py b/tests/components/test_group.py index e82190a3f29..6c601a411fb 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -1,5 +1,6 @@ """The tests for the Group components.""" # pylint: disable=protected-access,too-many-public-methods +from collections import OrderedDict import unittest from unittest.mock import patch @@ -220,16 +221,16 @@ class TestComponentsGroup(unittest.TestCase): test_group = group.Group( self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False) - _setup_component(self.hass, 'group', {'group': { - 'second_group': { + group_conf = OrderedDict() + group_conf['second_group'] = { 'entities': 'light.Bowl, ' + test_group.entity_id, 'icon': 'mdi:work', 'view': True, - }, - 'test_group': 'hello.world,sensor.happy', - 'empty_group': {'name': 'Empty Group', 'entities': None}, - } - }) + } + group_conf['test_group'] = 'hello.world,sensor.happy' + group_conf['empty_group'] = {'name': 'Empty Group', 'entities': None} + + _setup_component(self.hass, 'group', {'group': group_conf}) group_state = self.hass.states.get( group.ENTITY_ID_FORMAT.format('second_group')) @@ -241,6 +242,7 @@ class TestComponentsGroup(unittest.TestCase): group_state.attributes.get(ATTR_ICON)) self.assertTrue(group_state.attributes.get(group.ATTR_VIEW)) self.assertTrue(group_state.attributes.get(ATTR_HIDDEN)) + self.assertEqual(1, group_state.attributes.get(group.ATTR_ORDER)) group_state = self.hass.states.get( group.ENTITY_ID_FORMAT.format('test_group')) @@ -251,6 +253,7 @@ class TestComponentsGroup(unittest.TestCase): self.assertIsNone(group_state.attributes.get(ATTR_ICON)) self.assertIsNone(group_state.attributes.get(group.ATTR_VIEW)) self.assertIsNone(group_state.attributes.get(ATTR_HIDDEN)) + self.assertEqual(2, group_state.attributes.get(group.ATTR_ORDER)) def test_groups_get_unique_names(self): """Two groups with same name should both have a unique entity id.""" diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index d9da2c51da7..60b14757378 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1,3 +1,5 @@ +"""Test config validators.""" +from collections import OrderedDict from datetime import timedelta import os import tempfile @@ -367,3 +369,51 @@ def test_has_at_least_one_key(): for value in ({'beer': None}, {'soda': None}): schema(value) + + +def test_ordered_dict_order(): + """Test ordered_dict validator.""" + schema = vol.Schema(cv.ordered_dict(int, cv.string)) + + val = OrderedDict() + val['first'] = 1 + val['second'] = 2 + + validated = schema(val) + + assert isinstance(validated, OrderedDict) + assert ['first', 'second'] == list(validated.keys()) + + +def test_ordered_dict_key_validator(): + """Test ordered_dict key validator.""" + schema = vol.Schema(cv.ordered_dict(cv.match_all, cv.string)) + + with pytest.raises(vol.Invalid): + schema({None: 1}) + + schema({'hello': 'world'}) + + schema = vol.Schema(cv.ordered_dict(cv.match_all, int)) + + with pytest.raises(vol.Invalid): + schema({'hello': 1}) + + schema({1: 'works'}) + + +def test_ordered_dict_value_validator(): + """Test ordered_dict validator.""" + schema = vol.Schema(cv.ordered_dict(cv.string)) + + with pytest.raises(vol.Invalid): + schema({'hello': None}) + + schema({'hello': 'world'}) + + schema = vol.Schema(cv.ordered_dict(int)) + + with pytest.raises(vol.Invalid): + schema({'hello': 'world'}) + + schema({'hello': 5})