Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions homeassistant/components/freedns/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import voluptuous as vol

from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN,
CONF_UPDATE_INTERVAL)
CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL)
import homeassistant.helpers.config_validation as cv

_LOGGER = logging.getLogger(__name__)
Expand All @@ -26,21 +26,31 @@
UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php'

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Exclusive(CONF_URL, DOMAIN): cv.string,
vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string,
vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta),

})
DOMAIN: vol.All(
vol.Schema({
vol.Exclusive(CONF_URL, DOMAIN): cv.string,
vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string,
vol.Optional(CONF_UPDATE_INTERVAL):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_SCAN_INTERVAL):
vol.All(cv.time_period, cv.positive_timedelta),
}),
cv.deprecated(
CONF_UPDATE_INTERVAL,
replacement_key=CONF_SCAN_INTERVAL,
invalidation_version='1.0.0',
default=DEFAULT_INTERVAL
)
)
}, extra=vol.ALLOW_EXTRA)


async def async_setup(hass, config):
"""Initialize the FreeDNS component."""
url = config[DOMAIN].get(CONF_URL)
auth_token = config[DOMAIN].get(CONF_ACCESS_TOKEN)
update_interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL)
conf = config[DOMAIN]
url = conf.get(CONF_URL)
auth_token = conf.get(CONF_ACCESS_TOKEN)
update_interval = conf[CONF_SCAN_INTERVAL]

session = hass.helpers.aiohttp_client.async_get_clientsession()

Expand Down
113 changes: 96 additions & 17 deletions homeassistant/helpers/config_validation.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -67,6 +69,22 @@ def validate(obj: Dict) -> Dict:
return validate


def has_at_most_one_key(*keys: str) -> Callable:
Comment thread
MartinHjelmare marked this conversation as resolved.
"""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):
Expand Down Expand Up @@ -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
Comment thread
MartinHjelmare marked this conversation as resolved.
- 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])

return config
value = config[key]
check_for_invalid_version(value)
KeywordStyleAdapter(logging.getLogger(module_name)).warning(
Comment thread
MartinHjelmare marked this conversation as resolved.
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 has_at_most_one_key(key, replacement_key)(config)

return validator

Expand Down
49 changes: 49 additions & 0 deletions homeassistant/helpers/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Helpers for logging allowing more advanced logging styles to be used."""
import inspect
import logging


class KeywordMessage:
"""
Represents a logging message with keyword arguments.

Adapted from: https://stackoverflow.com/a/24683360/2267718
"""

def __init__(self, fmt, args, kwargs):
"""Initialize a new BraceMessage object."""
self._fmt = fmt
self._args = args
self._kwargs = kwargs

def __str__(self):
"""Convert the object to a string for logging."""
return str(self._fmt).format(*self._args, **self._kwargs)


class KeywordStyleAdapter(logging.LoggerAdapter):
"""Represents an adapter wrapping the logger allowing KeywordMessages."""

def __init__(self, logger, extra=None):
"""Initialize a new StyleAdapter for the provided logger."""
super(KeywordStyleAdapter, self).__init__(logger, extra or {})

def log(self, level, msg, *args, **kwargs):
"""Log the message provided at the appropriate level."""
if self.isEnabledFor(level):
msg, log_kwargs = self.process(msg, kwargs)
self.logger._log( # pylint: disable=protected-access
level, KeywordMessage(msg, args, kwargs), (), **log_kwargs
)

def process(self, msg, kwargs):
"""Process the keyward args in preparation for logging."""
return (
msg,
{
k: kwargs[k]
for k in inspect.getfullargspec(
self.logger._log # pylint: disable=protected-access
).args[1:] if k in kwargs
}
)
2 changes: 1 addition & 1 deletion tests/components/freedns/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_setup(hass, aioclient_mock):
result = yield from async_setup_component(hass, freedns.DOMAIN, {
freedns.DOMAIN: {
'access_token': ACCESS_TOKEN,
'update_interval': UPDATE_INTERVAL,
'scan_interval': UPDATE_INTERVAL,
}
})
assert result
Expand Down
Loading