diff --git a/esphome/config.py b/esphome/config.py index 5906e2fc95..53449c3e85 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -18,12 +18,12 @@ from esphome.components.substitutions import CONF_SUBSTITUTIONS from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS from esphome.core import CORE, EsphomeError # noqa from esphome.helpers import color, indent -from esphome.py_compat import text_type, IS_PY2, decode_text +from esphome.py_compat import text_type, IS_PY2, decode_text, string_types from esphome.util import safe_print, OrderedDict from typing import List, Optional, Tuple, Union # noqa from esphome.core import ConfigType # noqa -from esphome.yaml_util import is_secret, ESPHomeDataBase +from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue from esphome.voluptuous_schema import ExtraKeysInvalid _LOGGER = logging.getLogger(__name__) @@ -380,6 +380,24 @@ def do_id_pass(result): # type: (Config) -> None result.add_str_error("Couldn't resolve ID for type '{}'".format(id.type), path) +def recursive_check_replaceme(value): + import esphome.config_validation as cv + + if isinstance(value, list): + return cv.Schema([recursive_check_replaceme])(value) + if isinstance(value, dict): + return cv.Schema({cv.valid: recursive_check_replaceme})(value) + if isinstance(value, ESPForceValue): + pass + if isinstance(value, string_types) and value == 'REPLACEME': + raise cv.Invalid("Found 'REPLACEME' in configuration, this is most likely an error. " + "Please make sure you have replaced all fields from the sample " + "configuration.\n" + "If you want to use the literal REPLACEME string, " + "please use \"!force REPLACEME\"") + return value + + def validate_config(config): result = Config() @@ -393,6 +411,12 @@ def validate_config(config): result.add_error(err) return result + # 1.1. Check for REPLACEME special value + try: + recursive_check_replaceme(config) + except vol.Invalid as err: + result.add_error(err) + if 'esphomeyaml' in config: _LOGGER.warning("The esphomeyaml section has been renamed to esphome in 1.11.0. " "Please replace 'esphomeyaml:' in your configuration with 'esphome:'.") @@ -588,7 +612,7 @@ def _nested_getitem(data, path): def humanize_error(config, validation_error): validation_error = text_type(validation_error) - m = re.match(r'^(.*?)\s*(?:for dictionary value )?@ data\[.*$', validation_error) + m = re.match(r'^(.*?)\s*(?:for dictionary value )?@ data\[.*$', validation_error, re.DOTALL) if m is not None: validation_error = m.group(1) validation_error = validation_error.strip() diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 5c7255a874..88fb55e841 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -19,7 +19,7 @@ from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, CONF_TYPE from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \ TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes -from esphome.helpers import list_starts_with +from esphome.helpers import list_starts_with, add_class_to_obj from esphome.py_compat import integer_types, string_types, text_type, IS_PY2, decode_text from esphome.voluptuous_schema import _Schema @@ -964,11 +964,8 @@ def enum(mapping, **kwargs): one_of_validator = one_of(*mapping, **kwargs) def validator(value): - from esphome.yaml_util import make_data_base - - value = make_data_base(one_of_validator(value)) - cls = value.__class__ - value.__class__ = cls.__class__(cls.__name__ + "Enum", (cls, core.EnumValue), {}) + value = one_of_validator(value) + value = add_class_to_obj(value, core.EnumValue) value.enum_value = mapping[value] return value diff --git a/esphome/helpers.py b/esphome/helpers.py index 48607dbff5..e91b13a735 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -261,3 +261,51 @@ def file_compare(path1, path2): if not blob1: # Reached end return True + + +# A dict of types that need to be converted to heaptypes before a class can be added +# to the object +_TYPE_OVERLOADS = { + int: type('int', (int,), dict()), + float: type('float', (float,), dict()), + str: type('str', (str,), dict()), + dict: type('dict', (str,), dict()), + list: type('list', (list,), dict()), +} + +if IS_PY2: + _TYPE_OVERLOADS[long] = type('long', (long,), dict()) + _TYPE_OVERLOADS[unicode] = type('unicode', (unicode,), dict()) + +# cache created classes here +_CLASS_LOOKUP = {} + + +def add_class_to_obj(value, cls): + """Add a class to a python type. + + This function modifies value so that it has cls as a basetype. + The value itself may be modified by this action! You must use the return + value of this function however, since some types need to be copied first (heaptypes). + """ + if isinstance(value, cls): + # If already is instance, do not add + return value + + try: + orig_cls = value.__class__ + key = (orig_cls, cls) + new_cls = _CLASS_LOOKUP.get(key) + if new_cls is None: + new_cls = orig_cls.__class__(orig_cls.__name__, (orig_cls, cls), {}) + _CLASS_LOOKUP[key] = new_cls + value.__class__ = new_cls + return value + except TypeError: + # Non heap type, look in overloads dict + for type_, func in _TYPE_OVERLOADS.items(): + # Use type() here, we only need to trigger if it's the exact type, + # as otherwise we don't need to overload the class + if type(value) is type_: # pylint: disable=unidiomatic-typecheck + return add_class_to_obj(func(value), cls) + raise diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index fb04d7d5b0..d80334cedf 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -2,6 +2,7 @@ from __future__ import print_function import fnmatch import functools +import inspect import logging import math import os @@ -13,6 +14,7 @@ import yaml.constructor from esphome import core from esphome.config_helpers import read_config_file from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange +from esphome.helpers import add_class_to_obj from esphome.py_compat import text_type, IS_PY2 from esphome.util import OrderedDict, filter_yaml_files @@ -26,14 +28,6 @@ _SECRET_CACHE = {} _SECRET_VALUES = {} -class NodeListClass(list): - pass - - -class NodeStrClass(text_type): - pass - - class ESPHomeDataBase(object): @property def esp_range(self): @@ -44,56 +38,25 @@ class ESPHomeDataBase(object): self._esp_range = DocumentRange.from_marks(node.start_mark, node.end_mark) -class ESPInt(int, ESPHomeDataBase): +class ESPForceValue(object): pass -class ESPFloat(float, ESPHomeDataBase): - pass - - -class ESPStr(str, ESPHomeDataBase): - pass - - -class ESPDict(OrderedDict, ESPHomeDataBase): - pass - - -class ESPList(list, ESPHomeDataBase): - pass - - -class ESPLambda(Lambda, ESPHomeDataBase): - pass - - -ESP_TYPES = { - int: ESPInt, - float: ESPFloat, - str: ESPStr, - dict: ESPDict, - list: ESPList, - Lambda: ESPLambda, -} -if IS_PY2: - class ESPUnicode(unicode, ESPHomeDataBase): - pass - - ESP_TYPES[unicode] = ESPUnicode - - def make_data_base(value): - for typ, cons in ESP_TYPES.items(): - if isinstance(value, typ): - return cons(value) - return value + return add_class_to_obj(value, ESPHomeDataBase) def _add_data_ref(fn): @functools.wraps(fn) def wrapped(loader, node): res = fn(loader, node) + # newer PyYAML versions use generators, resolve them + if inspect.isgenerator(res): + generator = res + res = next(generator) + # Let generator finish + for _ in generator: + pass res = make_data_base(res) if isinstance(res, ESPHomeDataBase): res.from_node(node) @@ -296,6 +259,11 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors def construct_lambda(self, node): return Lambda(text_type(node.value)) + @_add_data_ref + def construct_force(self, node): + obj = self.construct_scalar(node) + return add_class_to_obj(obj, ESPForceValue) + ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:int', ESPHomeLoader.construct_yaml_int) ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:float', ESPHomeLoader.construct_yaml_float) @@ -314,6 +282,7 @@ ESPHomeLoader.add_constructor('!include_dir_named', ESPHomeLoader.construct_incl ESPHomeLoader.add_constructor('!include_dir_merge_named', ESPHomeLoader.construct_include_dir_merge_named) ESPHomeLoader.add_constructor('!lambda', ESPHomeLoader.construct_lambda) +ESPHomeLoader.add_constructor('!force', ESPHomeLoader.construct_force) def load_yaml(fname):