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:
parent
ee3631e93e
commit
d5fad33599
5 changed files with 487 additions and 40 deletions
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue