Add better handling of deprecated configs (#20565)

* Add better handling of deprecated configs

* Embed the call to has_at_most_one_key in deprecated

* Add tests for checking the deprecated logs

* Add thoroughly documented tests

* Always check has_at_most_one_key

* Fix typing

* Move logging helpers to homea new logging helper

* Lint

* Rename to KeywordMessage instead of BraceMessage

* Remove unneeded KeywordStyleAdapter

* Lint

* Use dict directly rather than dict.keys() when creating set

* Patch the version in unit tests, update logging and use parse_version

* Re-add KeywordStyleAdapter and fix tests

* Lint

* Lint
This commit is contained in:
Rohan Kapoor 2019-02-08 02:14:50 -08:00 committed by Martin Hjelmare
parent ee3631e93e
commit d5fad33599
5 changed files with 487 additions and 40 deletions

View file

@ -1,27 +1,29 @@
"""Helpers for config validation using voluptuous."""
from datetime import (timedelta, datetime as datetime_sys,
time as time_sys, date as date_sys)
import inspect
import logging
import os
import re
from urllib.parse import urlparse
from datetime import (timedelta, datetime as datetime_sys,
time as time_sys, date as date_sys)
from socket import _GLOBAL_DEFAULT_TIMEOUT
import logging
import inspect
from typing import Any, Union, TypeVar, Callable, Sequence, Dict
from typing import Any, Union, TypeVar, Callable, Sequence, Dict, Optional
from urllib.parse import urlparse
import voluptuous as vol
from pkg_resources import parse_version
import homeassistant.util.dt as dt_util
from homeassistant.const import (
CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT,
CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS,
CONF_CONDITION, CONF_BELOW, CONF_ABOVE, CONF_TIMEOUT, SUN_EVENT_SUNSET,
SUN_EVENT_SUNRISE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC,
ENTITY_MATCH_ALL, CONF_ENTITY_NAMESPACE)
ENTITY_MATCH_ALL, CONF_ENTITY_NAMESPACE, __version__)
from homeassistant.core import valid_entity_id, split_entity_id
from homeassistant.exceptions import TemplateError
import homeassistant.util.dt as dt_util
from homeassistant.util import slugify as util_slugify
from homeassistant.helpers import template as template_helper
from homeassistant.helpers.logging import KeywordStyleAdapter
from homeassistant.util import slugify as util_slugify
# pylint: disable=invalid-name
@ -67,6 +69,22 @@ def has_at_least_one_key(*keys: str) -> Callable:
return validate
def has_at_most_one_key(*keys: str) -> Callable:
"""Validate that zero keys exist or one key exists."""
def validate(obj: Dict) -> Dict:
"""Test zero keys exist or one key exists in dict."""
if not isinstance(obj, dict):
raise vol.Invalid('expected dictionary')
if len(set(keys) & set(obj)) > 1:
raise vol.Invalid(
'must contain at most one of {}.'.format(', '.join(keys))
)
return obj
return validate
def boolean(value: Any) -> bool:
"""Validate and coerce a boolean value."""
if isinstance(value, str):
@ -520,18 +538,79 @@ def ensure_list_csv(value: Any) -> Sequence:
return ensure_list(value)
def deprecated(key):
"""Log key as deprecated."""
def deprecated(key: str,
replacement_key: Optional[str] = None,
invalidation_version: Optional[str] = None,
default: Optional[Any] = None):
"""
Log key as deprecated and provide a replacement (if exists).
Expected behavior:
- Outputs the appropriate deprecation warning if key is detected
- Processes schema moving the value from key to replacement_key
- Processes schema changing nothing if only replacement_key provided
- No warning if only replacement_key provided
- No warning if neither key nor replacement_key are provided
- Adds replacement_key with default value in this case
- Once the invalidation_version is crossed, raises vol.Invalid if key
is detected
"""
module_name = inspect.getmodule(inspect.stack()[1][0]).__name__
def validator(config):
if replacement_key and invalidation_version:
warning = ("The '{key}' option (with value '{value}') is"
" deprecated, please replace it with '{replacement_key}'."
" This option will become invalid in version"
" {invalidation_version}")
elif replacement_key:
warning = ("The '{key}' option (with value '{value}') is"
" deprecated, please replace it with '{replacement_key}'")
elif invalidation_version:
warning = ("The '{key}' option (with value '{value}') is"
" deprecated, please remove it from your configuration."
" This option will become invalid in version"
" {invalidation_version}")
else:
warning = ("The '{key}' option (with value '{value}') is"
" deprecated, please remove it from your configuration")
def check_for_invalid_version(value: Optional[Any]):
"""Raise error if current version has reached invalidation."""
if not invalidation_version:
return
if parse_version(__version__) >= parse_version(invalidation_version):
raise vol.Invalid(
warning.format(
key=key,
value=value,
replacement_key=replacement_key,
invalidation_version=invalidation_version
)
)
def validator(config: Dict):
"""Check if key is in config and log warning."""
if key in config:
logging.getLogger(module_name).warning(
"The '%s' option (with value '%s') is deprecated, please "
"remove it from your configuration.", key, config[key])
value = config[key]
check_for_invalid_version(value)
KeywordStyleAdapter(logging.getLogger(module_name)).warning(
warning,
key=key,
value=value,
replacement_key=replacement_key,
invalidation_version=invalidation_version
)
if replacement_key:
config.pop(key)
else:
value = default
if (replacement_key
and replacement_key not in config
and value is not None):
config[replacement_key] = value
return config
return has_at_most_one_key(key, replacement_key)(config)
return validator