Mqtt fan fail deprecated options for classic speeds (#58992)

* Fail deprecated options

* new removed validator

* correct module_name - add tests

* Add test cant find module cv.removed

* module name from stack+1

* Remove error from log. Just throw.

* assert on thrown exception text

* cleanup formatting remove KeyStyleAdapter

* format the replacement_key and update test

* deprecated vs removed - add raise_if_present opt

* doc string update

* is deprecated
This commit is contained in:
Jan Bouwhuis 2021-11-04 17:54:27 +01:00 committed by GitHub
parent 7945facf1e
commit c3fc19915e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 152 additions and 38 deletions

View file

@ -175,13 +175,13 @@ PLATFORM_SCHEMA = vol.All(
# CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LIST, CONF_SPEED_STATE_TOPIC, CONF_SPEED_VALUE_TEMPLATE and # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LIST, CONF_SPEED_STATE_TOPIC, CONF_SPEED_VALUE_TEMPLATE and
# Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF, # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF,
# are deprecated, support will be removed with release 2021.9 # are deprecated, support will be removed with release 2021.9
cv.deprecated(CONF_PAYLOAD_HIGH_SPEED), cv.removed(CONF_PAYLOAD_HIGH_SPEED),
cv.deprecated(CONF_PAYLOAD_LOW_SPEED), cv.removed(CONF_PAYLOAD_LOW_SPEED),
cv.deprecated(CONF_PAYLOAD_MEDIUM_SPEED), cv.removed(CONF_PAYLOAD_MEDIUM_SPEED),
cv.deprecated(CONF_SPEED_COMMAND_TOPIC), cv.removed(CONF_SPEED_COMMAND_TOPIC),
cv.deprecated(CONF_SPEED_LIST), cv.removed(CONF_SPEED_LIST),
cv.deprecated(CONF_SPEED_STATE_TOPIC), cv.removed(CONF_SPEED_STATE_TOPIC),
cv.deprecated(CONF_SPEED_VALUE_TEMPLATE), cv.removed(CONF_SPEED_VALUE_TEMPLATE),
_PLATFORM_SCHEMA_BASE, _PLATFORM_SCHEMA_BASE,
valid_speed_range_configuration, valid_speed_range_configuration,
valid_preset_mode_configuration, valid_preset_mode_configuration,
@ -191,13 +191,13 @@ DISCOVERY_SCHEMA = vol.All(
# CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LIST, CONF_SPEED_STATE_TOPIC, CONF_SPEED_VALUE_TEMPLATE and # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LIST, CONF_SPEED_STATE_TOPIC, CONF_SPEED_VALUE_TEMPLATE and
# Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF, # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF,
# are deprecated, support will be removed with release 2021.9 # are deprecated, support will be removed with release 2021.9
cv.deprecated(CONF_PAYLOAD_HIGH_SPEED), cv.removed(CONF_PAYLOAD_HIGH_SPEED),
cv.deprecated(CONF_PAYLOAD_LOW_SPEED), cv.removed(CONF_PAYLOAD_LOW_SPEED),
cv.deprecated(CONF_PAYLOAD_MEDIUM_SPEED), cv.removed(CONF_PAYLOAD_MEDIUM_SPEED),
cv.deprecated(CONF_SPEED_COMMAND_TOPIC), cv.removed(CONF_SPEED_COMMAND_TOPIC),
cv.deprecated(CONF_SPEED_LIST), cv.removed(CONF_SPEED_LIST),
cv.deprecated(CONF_SPEED_STATE_TOPIC), cv.removed(CONF_SPEED_STATE_TOPIC),
cv.deprecated(CONF_SPEED_VALUE_TEMPLATE), cv.removed(CONF_SPEED_VALUE_TEMPLATE),
_PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
valid_speed_range_configuration, valid_speed_range_configuration,
valid_preset_mode_configuration, valid_preset_mode_configuration,

View file

@ -78,7 +78,6 @@ from homeassistant.helpers import (
script_variables as script_variables_helper, script_variables as script_variables_helper,
template as template_helper, template as template_helper,
) )
from homeassistant.helpers.logging import KeywordStyleAdapter
from homeassistant.util import raise_if_invalid_path, slugify as util_slugify from homeassistant.util import raise_if_invalid_path, slugify as util_slugify
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -709,23 +708,26 @@ class multi_select:
return selected return selected
def deprecated( def _deprecated_or_removed(
key: str, key: str,
replacement_key: str | None = None, replacement_key: str | None = None,
default: Any | None = None, default: Any | None = None,
raise_if_present: bool | None = False,
option_status: str | None = "is deprecated",
) -> Callable[[dict], dict]: ) -> Callable[[dict], dict]:
""" """
Log key as deprecated and provide a replacement (if exists). Log key as deprecated and provide a replacement (if exists) or fail.
Expected behavior: Expected behavior:
- Outputs the appropriate deprecation warning if key is detected - Outputs or throws the appropriate deprecation warning if key is detected
- Outputs or throws the appropriate error if key is detected and removed from support
- Processes schema moving the value from key to replacement_key - Processes schema moving the value from key to replacement_key
- Processes schema changing nothing if only replacement_key provided - Processes schema changing nothing if only replacement_key provided
- No warning if only replacement_key provided - No warning if only replacement_key provided
- No warning if neither key nor replacement_key are provided - No warning if neither key nor replacement_key are provided
- Adds replacement_key with default value in this case - Adds replacement_key with default value in this case
""" """
module = inspect.getmodule(inspect.stack(context=0)[1].frame) module = inspect.getmodule(inspect.stack(context=0)[2].frame)
if module is not None: if module is not None:
module_name = module.__name__ module_name = module.__name__
else: else:
@ -733,36 +735,36 @@ def deprecated(
# will be missing information, so let's guard. # will be missing information, so let's guard.
# https://github.com/home-assistant/core/issues/24982 # https://github.com/home-assistant/core/issues/24982
module_name = __name__ module_name = __name__
if replacement_key: if replacement_key:
warning = ( warning = (
"The '{key}' option is deprecated," "The '{key}' option {option_status},"
" please replace it with '{replacement_key}'" " please replace it with '{replacement_key}'"
) )
else: else:
warning = ( warning = (
"The '{key}' option is deprecated," "The '{key}' option {option_status},"
" please remove it from your configuration" " please remove it from your configuration"
) )
def validator(config: dict) -> dict: def validator(config: dict) -> dict:
"""Check if key is in config and log warning.""" """Check if key is in config and log warning or error."""
if key in config: if key in config:
try: try:
KeywordStyleAdapter(logging.getLogger(module_name)).warning( warning_local = warning.replace(
warning.replace( "'{key}' option",
"'{key}' option", f"'{key}' option near {config.__config_file__}:{config.__line__}", # type: ignore
f"'{key}' option near {config.__config_file__}:{config.__line__}", # type: ignore
),
key=key,
replacement_key=replacement_key,
) )
except AttributeError: except AttributeError:
KeywordStyleAdapter(logging.getLogger(module_name)).warning( warning_local = warning
warning, warning_local = warning_local.format(
key=key, key=key,
replacement_key=replacement_key, replacement_key=replacement_key,
) option_status=option_status,
)
if raise_if_present:
raise vol.Invalid(warning_local)
logging.getLogger(module_name).warning(warning_local)
value = config[key] value = config[key]
if replacement_key: if replacement_key:
config.pop(key) config.pop(key)
@ -782,6 +784,52 @@ def deprecated(
return validator return validator
def deprecated(
key: str,
replacement_key: str | None = None,
default: Any | None = None,
raise_if_present: bool | None = False,
) -> Callable[[dict], dict]:
"""
Log key as deprecated and provide a replacement (if exists).
Expected behavior:
- Outputs the appropriate deprecation warning if key is detected or raises an exception
- 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
"""
return _deprecated_or_removed(
key,
replacement_key=replacement_key,
default=default,
raise_if_present=raise_if_present,
option_status="is deprecated",
)
def removed(
key: str,
default: Any | None = None,
raise_if_present: bool | None = True,
) -> Callable[[dict], dict]:
"""
Log key as deprecated and fail the config validation.
Expected behavior:
- Outputs the appropriate error if key is detected and removed from support or raises an exception
"""
return _deprecated_or_removed(
key,
replacement_key=None,
default=default,
raise_if_present=raise_if_present,
option_status="was removed",
)
def key_value_schemas( def key_value_schemas(
key: str, value_schemas: dict[Hashable, vol.Schema] key: str, value_schemas: dict[Hashable, vol.Schema]
) -> Callable[[Any], dict[Hashable, Any]]: ) -> Callable[[Any], dict[Hashable, Any]]:

View file

@ -712,6 +712,46 @@ def test_deprecated_with_no_optionals(caplog, schema):
assert test_data == output assert test_data == output
def test_deprecated_or_removed_param_and_raise(caplog, schema):
"""
Test removed or deprecation options and fail the config validation by raising an exception.
Expected behavior:
- Outputs the appropriate deprecation or removed from support error if key is detected
"""
removed_schema = vol.All(cv.deprecated("mars", raise_if_present=True), schema)
test_data = {"mars": True}
with pytest.raises(vol.Invalid) as excinfo:
removed_schema(test_data)
assert (
"The 'mars' option is deprecated, please remove it from your configuration"
in str(excinfo.value)
)
assert len(caplog.records) == 0
test_data = {"venus": True}
output = removed_schema(test_data.copy())
assert len(caplog.records) == 0
assert test_data == output
deprecated_schema = vol.All(cv.removed("mars"), schema)
test_data = {"mars": True}
with pytest.raises(vol.Invalid) as excinfo:
deprecated_schema(test_data)
assert (
"The 'mars' option was removed, please remove it from your configuration"
in str(excinfo.value)
)
assert len(caplog.records) == 0
test_data = {"venus": True}
output = deprecated_schema(test_data.copy())
assert len(caplog.records) == 0
assert test_data == output
def test_deprecated_with_replacement_key(caplog, schema): def test_deprecated_with_replacement_key(caplog, schema):
""" """
Test deprecation behaves correctly when only a replacement key is provided. Test deprecation behaves correctly when only a replacement key is provided.
@ -846,17 +886,43 @@ def test_deprecated_cant_find_module():
default=False, default=False,
) )
with patch("inspect.getmodule", return_value=None):
# This used to raise.
cv.removed(
"mars",
default=False,
)
def test_deprecated_logger_with_config_attributes(caplog):
def test_deprecated_or_removed_logger_with_config_attributes(caplog):
"""Test if the logger outputs the correct message if the line and file attribute is available in config.""" """Test if the logger outputs the correct message if the line and file attribute is available in config."""
file: str = "configuration.yaml" file: str = "configuration.yaml"
line: int = 54 line: int = 54
replacement = f"'mars' option near {file}:{line} is deprecated"
# test as deprecated option
replacement_key = "jupiter"
option_status = "is deprecated"
replacement = f"'mars' option near {file}:{line} {option_status}, please replace it with '{replacement_key}'"
config = OrderedDict([("mars", "blah")]) config = OrderedDict([("mars", "blah")])
setattr(config, "__config_file__", file) setattr(config, "__config_file__", file)
setattr(config, "__line__", line) setattr(config, "__line__", line)
cv.deprecated("mars", replacement_key="jupiter", default=False)(config) cv.deprecated("mars", replacement_key=replacement_key, default=False)(config)
assert len(caplog.records) == 1
assert replacement in caplog.text
caplog.clear()
assert len(caplog.records) == 0
# test as removed option
option_status = "was removed"
replacement = f"'mars' option near {file}:{line} {option_status}, please remove it from your configuration"
config = OrderedDict([("mars", "blah")])
setattr(config, "__config_file__", file)
setattr(config, "__line__", line)
cv.removed("mars", default=False, raise_if_present=False)(config)
assert len(caplog.records) == 1 assert len(caplog.records) == 1
assert replacement in caplog.text assert replacement in caplog.text