Raise and suppress stack trace when reloading yaml fails (#102410)
* Allow async_integration_yaml_config to raise * Docstr - split check * Implement as wrapper, return dataclass * Fix setup error handling * Fix reload test mock * Move log_messages to error handler * Remove unreachable code * Remove config test helper * Refactor and ensure notifications during setup * Remove redundat error, adjust tests notifications * Fix patch * Apply suggestions from code review Co-authored-by: Erik Montnemery <erik@montnemery.com> * Follow up comments * Add call_back decorator * Split long lines * Update exception abbreviations --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
852fb58ca8
commit
af71c2bb45
15 changed files with 954 additions and 195 deletions
|
@ -194,7 +194,9 @@ async def async_setup_platform(
|
||||||
|
|
||||||
integration = await async_get_integration(hass, SCENE_DOMAIN)
|
integration = await async_get_integration(hass, SCENE_DOMAIN)
|
||||||
|
|
||||||
conf = await conf_util.async_process_component_config(hass, config, integration)
|
conf = await conf_util.async_process_component_and_handle_errors(
|
||||||
|
hass, config, integration
|
||||||
|
)
|
||||||
|
|
||||||
if not (conf and platform):
|
if not (conf and platform):
|
||||||
return
|
return
|
||||||
|
|
|
@ -138,6 +138,36 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
|
"component_import_err": {
|
||||||
|
"message": "Unable to import {domain}: {error}"
|
||||||
|
},
|
||||||
|
"config_platform_import_err": {
|
||||||
|
"message": "Error importing config platform {domain}: {error}"
|
||||||
|
},
|
||||||
|
"config_validation_err": {
|
||||||
|
"message": "Invalid config for integration {domain} at {config_file}, line {line}: {error}. Check the logs for more information."
|
||||||
|
},
|
||||||
|
"config_validator_unknown_err": {
|
||||||
|
"message": "Unknown error calling {domain} config validator. Check the logs for more information."
|
||||||
|
},
|
||||||
|
"config_schema_unknown_err": {
|
||||||
|
"message": "Unknown error calling {domain} CONFIG_SCHEMA. Check the logs for more information."
|
||||||
|
},
|
||||||
|
"integration_config_error": {
|
||||||
|
"message": "Failed to process config for integration {domain} due to multiple ({errors}) errors. Check the logs for more information."
|
||||||
|
},
|
||||||
|
"platform_component_load_err": {
|
||||||
|
"message": "Platform error: {domain} - {error}. Check the logs for more information."
|
||||||
|
},
|
||||||
|
"platform_component_load_exc": {
|
||||||
|
"message": "Platform error: {domain} - {error}. Check the logs for more information."
|
||||||
|
},
|
||||||
|
"platform_config_validation_err": {
|
||||||
|
"message": "Invalid config for {domain} from integration {p_name} at file {config_file}, line {line}: {error}. Check the logs for more information."
|
||||||
|
},
|
||||||
|
"platform_schema_validator_err": {
|
||||||
|
"message": "Unknown error when validating config for {domain} from integration {p_name}"
|
||||||
|
},
|
||||||
"service_not_found": {
|
"service_not_found": {
|
||||||
"message": "Service {domain}.{service} not found."
|
"message": "Service {domain}.{service} not found."
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,10 @@ import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import frontend, websocket_api
|
from homeassistant.components import frontend, websocket_api
|
||||||
from homeassistant.config import async_hass_config_yaml, async_process_component_config
|
from homeassistant.config import (
|
||||||
|
async_hass_config_yaml,
|
||||||
|
async_process_component_and_handle_errors,
|
||||||
|
)
|
||||||
from homeassistant.const import CONF_FILENAME, CONF_MODE, CONF_RESOURCES
|
from homeassistant.const import CONF_FILENAME, CONF_MODE, CONF_RESOURCES
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
@ -85,7 +88,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
|
||||||
integration = await async_get_integration(hass, DOMAIN)
|
integration = await async_get_integration(hass, DOMAIN)
|
||||||
|
|
||||||
config = await async_process_component_config(hass, conf, integration)
|
config = await async_process_component_and_handle_errors(
|
||||||
|
hass, conf, integration
|
||||||
|
)
|
||||||
|
|
||||||
if config is None:
|
if config is None:
|
||||||
raise HomeAssistantError("Config validation failed")
|
raise HomeAssistantError("Config validation failed")
|
||||||
|
|
|
@ -25,7 +25,7 @@ from homeassistant.const import (
|
||||||
)
|
)
|
||||||
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
|
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import (
|
||||||
HomeAssistantError,
|
ConfigValidationError,
|
||||||
ServiceValidationError,
|
ServiceValidationError,
|
||||||
TemplateError,
|
TemplateError,
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
|
@ -417,14 +417,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
async def _reload_config(call: ServiceCall) -> None:
|
async def _reload_config(call: ServiceCall) -> None:
|
||||||
"""Reload the platforms."""
|
"""Reload the platforms."""
|
||||||
# Fetch updated manually configured items and validate
|
# Fetch updated manually configured items and validate
|
||||||
if (
|
try:
|
||||||
config_yaml := await async_integration_yaml_config(hass, DOMAIN)
|
config_yaml = await async_integration_yaml_config(
|
||||||
) is None:
|
hass, DOMAIN, raise_on_failure=True
|
||||||
# Raise in case we have an invalid configuration
|
|
||||||
raise HomeAssistantError(
|
|
||||||
"Error reloading manually configured MQTT items, "
|
|
||||||
"check your configuration.yaml"
|
|
||||||
)
|
)
|
||||||
|
except ConfigValidationError as ex:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
str(ex),
|
||||||
|
translation_domain=ex.translation_domain,
|
||||||
|
translation_key=ex.translation_key,
|
||||||
|
translation_placeholders=ex.translation_placeholders,
|
||||||
|
) from ex
|
||||||
|
|
||||||
# Check the schema before continuing reload
|
# Check the schema before continuing reload
|
||||||
await async_check_config_schema(hass, config_yaml)
|
await async_check_config_schema(hass, config_yaml)
|
||||||
|
|
||||||
|
|
|
@ -34,8 +34,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
_LOGGER.error(err)
|
_LOGGER.error(err)
|
||||||
return
|
return
|
||||||
|
|
||||||
conf = await conf_util.async_process_component_config(
|
integration = await async_get_integration(hass, DOMAIN)
|
||||||
hass, unprocessed_conf, await async_get_integration(hass, DOMAIN)
|
conf = await conf_util.async_process_component_and_handle_errors(
|
||||||
|
hass, unprocessed_conf, integration
|
||||||
)
|
)
|
||||||
|
|
||||||
if conf is None:
|
if conf is None:
|
||||||
|
|
|
@ -4,6 +4,8 @@ from __future__ import annotations
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from collections.abc import Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import StrEnum
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
|
@ -12,7 +14,7 @@ from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
|
@ -54,7 +56,7 @@ from .const import (
|
||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
from .core import DOMAIN as CONF_CORE, ConfigSource, HomeAssistant, callback
|
from .core import DOMAIN as CONF_CORE, ConfigSource, HomeAssistant, callback
|
||||||
from .exceptions import HomeAssistantError
|
from .exceptions import ConfigValidationError, HomeAssistantError
|
||||||
from .generated.currencies import HISTORIC_CURRENCIES
|
from .generated.currencies import HISTORIC_CURRENCIES
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
config_per_platform,
|
config_per_platform,
|
||||||
|
@ -66,13 +68,13 @@ from .helpers.entity_values import EntityValues
|
||||||
from .helpers.typing import ConfigType
|
from .helpers.typing import ConfigType
|
||||||
from .loader import ComponentProtocol, Integration, IntegrationNotFound
|
from .loader import ComponentProtocol, Integration, IntegrationNotFound
|
||||||
from .requirements import RequirementsNotFound, async_get_integration_with_requirements
|
from .requirements import RequirementsNotFound, async_get_integration_with_requirements
|
||||||
|
from .setup import async_notify_setup_error
|
||||||
from .util.package import is_docker_env
|
from .util.package import is_docker_env
|
||||||
from .util.unit_system import get_unit_system, validate_unit_system
|
from .util.unit_system import get_unit_system, validate_unit_system
|
||||||
from .util.yaml import SECRET_YAML, Secrets, load_yaml
|
from .util.yaml import SECRET_YAML, Secrets, load_yaml
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_PERSISTENT_ERRORS = "bootstrap_persistent_errors"
|
|
||||||
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
|
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
|
||||||
RE_ASCII = re.compile(r"\033\[[^m]*m")
|
RE_ASCII = re.compile(r"\033\[[^m]*m")
|
||||||
YAML_CONFIG_FILE = "configuration.yaml"
|
YAML_CONFIG_FILE = "configuration.yaml"
|
||||||
|
@ -117,6 +119,46 @@ tts:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigErrorTranslationKey(StrEnum):
|
||||||
|
"""Config error translation keys for config errors."""
|
||||||
|
|
||||||
|
# translation keys with a generated config related message text
|
||||||
|
CONFIG_VALIDATION_ERR = "config_validation_err"
|
||||||
|
PLATFORM_CONFIG_VALIDATION_ERR = "platform_config_validation_err"
|
||||||
|
|
||||||
|
# translation keys with a general static message text
|
||||||
|
COMPONENT_IMPORT_ERR = "component_import_err"
|
||||||
|
CONFIG_PLATFORM_IMPORT_ERR = "config_platform_import_err"
|
||||||
|
CONFIG_VALIDATOR_UNKNOWN_ERR = "config_validator_unknown_err"
|
||||||
|
CONFIG_SCHEMA_UNKNOWN_ERR = "config_schema_unknown_err"
|
||||||
|
PLATFORM_VALIDATOR_UNKNOWN_ERR = "platform_validator_unknown_err"
|
||||||
|
PLATFORM_COMPONENT_LOAD_ERR = "platform_component_load_err"
|
||||||
|
PLATFORM_COMPONENT_LOAD_EXC = "platform_component_load_exc"
|
||||||
|
PLATFORM_SCHEMA_VALIDATOR_ERR = "platform_schema_validator_err"
|
||||||
|
|
||||||
|
# translation key in case multiple errors occurred
|
||||||
|
INTEGRATION_CONFIG_ERROR = "integration_config_error"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ConfigExceptionInfo:
|
||||||
|
"""Configuration exception info class."""
|
||||||
|
|
||||||
|
exception: Exception
|
||||||
|
translation_key: ConfigErrorTranslationKey
|
||||||
|
platform_name: str
|
||||||
|
config: ConfigType
|
||||||
|
integration_link: str | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class IntegrationConfigInfo:
|
||||||
|
"""Configuration for an integration and exception information."""
|
||||||
|
|
||||||
|
config: ConfigType | None
|
||||||
|
exception_info_list: list[ConfigExceptionInfo]
|
||||||
|
|
||||||
|
|
||||||
def _no_duplicate_auth_provider(
|
def _no_duplicate_auth_provider(
|
||||||
configs: Sequence[dict[str, Any]]
|
configs: Sequence[dict[str, Any]]
|
||||||
) -> Sequence[dict[str, Any]]:
|
) -> Sequence[dict[str, Any]]:
|
||||||
|
@ -1025,21 +1067,193 @@ async def merge_packages_config(
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
async def async_process_component_config( # noqa: C901
|
@callback
|
||||||
hass: HomeAssistant, config: ConfigType, integration: Integration
|
def _get_log_message_and_stack_print_pref(
|
||||||
) -> ConfigType | None:
|
hass: HomeAssistant, domain: str, platform_exception: ConfigExceptionInfo
|
||||||
"""Check component configuration and return processed configuration.
|
) -> tuple[str | None, bool, dict[str, str]]:
|
||||||
|
"""Get message to log and print stack trace preference."""
|
||||||
|
exception = platform_exception.exception
|
||||||
|
platform_name = platform_exception.platform_name
|
||||||
|
platform_config = platform_exception.config
|
||||||
|
link = platform_exception.integration_link
|
||||||
|
|
||||||
Returns None on error.
|
placeholders: dict[str, str] = {"domain": domain, "error": str(exception)}
|
||||||
|
|
||||||
|
log_message_mapping: dict[ConfigErrorTranslationKey, tuple[str, bool]] = {
|
||||||
|
ConfigErrorTranslationKey.COMPONENT_IMPORT_ERR: (
|
||||||
|
f"Unable to import {domain}: {exception}",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR: (
|
||||||
|
f"Error importing config platform {domain}: {exception}",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
ConfigErrorTranslationKey.CONFIG_VALIDATOR_UNKNOWN_ERR: (
|
||||||
|
f"Unknown error calling {domain} config validator",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
ConfigErrorTranslationKey.CONFIG_SCHEMA_UNKNOWN_ERR: (
|
||||||
|
f"Unknown error calling {domain} CONFIG_SCHEMA",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
ConfigErrorTranslationKey.PLATFORM_VALIDATOR_UNKNOWN_ERR: (
|
||||||
|
f"Unknown error validating {platform_name} platform config with {domain} "
|
||||||
|
"component platform schema",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_ERR: (
|
||||||
|
f"Platform error: {domain} - {exception}",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC: (
|
||||||
|
f"Platform error: {domain} - {exception}",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR: (
|
||||||
|
f"Unknown error validating config for {platform_name} platform "
|
||||||
|
f"for {domain} component with PLATFORM_SCHEMA",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
log_message_show_stack_trace = log_message_mapping.get(
|
||||||
|
platform_exception.translation_key
|
||||||
|
)
|
||||||
|
if log_message_show_stack_trace is None:
|
||||||
|
# If no pre defined log_message is set, we generate an enriched error
|
||||||
|
# message, so we can notify about it during setup
|
||||||
|
show_stack_trace = False
|
||||||
|
if isinstance(exception, vol.Invalid):
|
||||||
|
log_message = format_schema_error(
|
||||||
|
hass, exception, platform_name, platform_config, link
|
||||||
|
)
|
||||||
|
if annotation := find_annotation(platform_config, exception.path):
|
||||||
|
placeholders["config_file"], line = annotation
|
||||||
|
placeholders["line"] = str(line)
|
||||||
|
else:
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert isinstance(exception, HomeAssistantError)
|
||||||
|
log_message = format_homeassistant_error(
|
||||||
|
hass, exception, platform_name, platform_config, link
|
||||||
|
)
|
||||||
|
if annotation := find_annotation(platform_config, [platform_name]):
|
||||||
|
placeholders["config_file"], line = annotation
|
||||||
|
placeholders["line"] = str(line)
|
||||||
|
show_stack_trace = True
|
||||||
|
return (log_message, show_stack_trace, placeholders)
|
||||||
|
|
||||||
|
assert isinstance(log_message_show_stack_trace, tuple)
|
||||||
|
|
||||||
|
return (*log_message_show_stack_trace, placeholders)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_process_component_and_handle_errors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: ConfigType,
|
||||||
|
integration: Integration,
|
||||||
|
raise_on_failure: bool = False,
|
||||||
|
) -> ConfigType | None:
|
||||||
|
"""Process and component configuration and handle errors.
|
||||||
|
|
||||||
|
In case of errors:
|
||||||
|
- Print the error messages to the log.
|
||||||
|
- Raise a ConfigValidationError if raise_on_failure is set.
|
||||||
|
|
||||||
|
Returns the integration config or `None`.
|
||||||
|
"""
|
||||||
|
integration_config_info = await async_process_component_config(
|
||||||
|
hass, config, integration
|
||||||
|
)
|
||||||
|
return async_handle_component_errors(
|
||||||
|
hass, integration_config_info, integration, raise_on_failure
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_handle_component_errors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
integration_config_info: IntegrationConfigInfo,
|
||||||
|
integration: Integration,
|
||||||
|
raise_on_failure: bool = False,
|
||||||
|
) -> ConfigType | None:
|
||||||
|
"""Handle component configuration errors from async_process_component_config.
|
||||||
|
|
||||||
|
In case of errors:
|
||||||
|
- Print the error messages to the log.
|
||||||
|
- Raise a ConfigValidationError if raise_on_failure is set.
|
||||||
|
|
||||||
|
Returns the integration config or `None`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not (config_exception_info := integration_config_info.exception_info_list):
|
||||||
|
return integration_config_info.config
|
||||||
|
|
||||||
|
platform_exception: ConfigExceptionInfo
|
||||||
|
domain = integration.domain
|
||||||
|
placeholders: dict[str, str]
|
||||||
|
for platform_exception in config_exception_info:
|
||||||
|
exception = platform_exception.exception
|
||||||
|
(
|
||||||
|
log_message,
|
||||||
|
show_stack_trace,
|
||||||
|
placeholders,
|
||||||
|
) = _get_log_message_and_stack_print_pref(hass, domain, platform_exception)
|
||||||
|
_LOGGER.error(
|
||||||
|
log_message,
|
||||||
|
exc_info=exception if show_stack_trace else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not raise_on_failure:
|
||||||
|
return integration_config_info.config
|
||||||
|
|
||||||
|
if len(config_exception_info) == 1:
|
||||||
|
translation_key = platform_exception.translation_key
|
||||||
|
else:
|
||||||
|
translation_key = ConfigErrorTranslationKey.INTEGRATION_CONFIG_ERROR
|
||||||
|
errors = str(len(config_exception_info))
|
||||||
|
log_message = (
|
||||||
|
f"Failed to process component config for integration {domain} "
|
||||||
|
f"due to multiple errors ({errors}), check the logs for more information."
|
||||||
|
)
|
||||||
|
placeholders = {
|
||||||
|
"domain": domain,
|
||||||
|
"errors": errors,
|
||||||
|
}
|
||||||
|
raise ConfigValidationError(
|
||||||
|
str(log_message),
|
||||||
|
[platform_exception.exception for platform_exception in config_exception_info],
|
||||||
|
translation_domain="homeassistant",
|
||||||
|
translation_key=translation_key,
|
||||||
|
translation_placeholders=placeholders,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_process_component_config( # noqa: C901
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: ConfigType,
|
||||||
|
integration: Integration,
|
||||||
|
) -> IntegrationConfigInfo:
|
||||||
|
"""Check component configuration.
|
||||||
|
|
||||||
|
Returns processed configuration and exception information.
|
||||||
|
|
||||||
This method must be run in the event loop.
|
This method must be run in the event loop.
|
||||||
"""
|
"""
|
||||||
domain = integration.domain
|
domain = integration.domain
|
||||||
|
integration_docs = integration.documentation
|
||||||
|
config_exceptions: list[ConfigExceptionInfo] = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
component = integration.get_component()
|
component = integration.get_component()
|
||||||
except LOAD_EXCEPTIONS as ex:
|
except LOAD_EXCEPTIONS as exc:
|
||||||
_LOGGER.error("Unable to import %s: %s", domain, ex)
|
exc_info = ConfigExceptionInfo(
|
||||||
return None
|
exc,
|
||||||
|
ConfigErrorTranslationKey.COMPONENT_IMPORT_ERR,
|
||||||
|
domain,
|
||||||
|
config,
|
||||||
|
integration_docs,
|
||||||
|
)
|
||||||
|
config_exceptions.append(exc_info)
|
||||||
|
return IntegrationConfigInfo(None, config_exceptions)
|
||||||
|
|
||||||
# Check if the integration has a custom config validator
|
# Check if the integration has a custom config validator
|
||||||
config_validator = None
|
config_validator = None
|
||||||
|
@ -1050,62 +1264,101 @@ async def async_process_component_config( # noqa: C901
|
||||||
# If the config platform contains bad imports, make sure
|
# If the config platform contains bad imports, make sure
|
||||||
# that still fails.
|
# that still fails.
|
||||||
if err.name != f"{integration.pkg_path}.config":
|
if err.name != f"{integration.pkg_path}.config":
|
||||||
_LOGGER.error("Error importing config platform %s: %s", domain, err)
|
exc_info = ConfigExceptionInfo(
|
||||||
return None
|
err,
|
||||||
|
ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR,
|
||||||
|
domain,
|
||||||
|
config,
|
||||||
|
integration_docs,
|
||||||
|
)
|
||||||
|
config_exceptions.append(exc_info)
|
||||||
|
return IntegrationConfigInfo(None, config_exceptions)
|
||||||
|
|
||||||
if config_validator is not None and hasattr(
|
if config_validator is not None and hasattr(
|
||||||
config_validator, "async_validate_config"
|
config_validator, "async_validate_config"
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
return ( # type: ignore[no-any-return]
|
return IntegrationConfigInfo(
|
||||||
await config_validator.async_validate_config(hass, config)
|
await config_validator.async_validate_config(hass, config), []
|
||||||
)
|
)
|
||||||
except (vol.Invalid, HomeAssistantError) as ex:
|
except (vol.Invalid, HomeAssistantError) as exc:
|
||||||
async_log_config_validator_error(
|
exc_info = ConfigExceptionInfo(
|
||||||
ex, domain, config, hass, integration.documentation
|
exc,
|
||||||
|
ConfigErrorTranslationKey.CONFIG_VALIDATION_ERR,
|
||||||
|
domain,
|
||||||
|
config,
|
||||||
|
integration_docs,
|
||||||
)
|
)
|
||||||
return None
|
config_exceptions.append(exc_info)
|
||||||
except Exception: # pylint: disable=broad-except
|
return IntegrationConfigInfo(None, config_exceptions)
|
||||||
_LOGGER.exception("Unknown error calling %s config validator", domain)
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
return None
|
exc_info = ConfigExceptionInfo(
|
||||||
|
exc,
|
||||||
|
ConfigErrorTranslationKey.CONFIG_VALIDATOR_UNKNOWN_ERR,
|
||||||
|
domain,
|
||||||
|
config,
|
||||||
|
integration_docs,
|
||||||
|
)
|
||||||
|
config_exceptions.append(exc_info)
|
||||||
|
return IntegrationConfigInfo(None, config_exceptions)
|
||||||
|
|
||||||
# No custom config validator, proceed with schema validation
|
# No custom config validator, proceed with schema validation
|
||||||
if hasattr(component, "CONFIG_SCHEMA"):
|
if hasattr(component, "CONFIG_SCHEMA"):
|
||||||
try:
|
try:
|
||||||
return component.CONFIG_SCHEMA(config) # type: ignore[no-any-return]
|
return IntegrationConfigInfo(component.CONFIG_SCHEMA(config), [])
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as exc:
|
||||||
async_log_schema_error(ex, domain, config, hass, integration.documentation)
|
exc_info = ConfigExceptionInfo(
|
||||||
return None
|
exc,
|
||||||
except Exception: # pylint: disable=broad-except
|
ConfigErrorTranslationKey.CONFIG_VALIDATION_ERR,
|
||||||
_LOGGER.exception("Unknown error calling %s CONFIG_SCHEMA", domain)
|
domain,
|
||||||
return None
|
config,
|
||||||
|
integration_docs,
|
||||||
|
)
|
||||||
|
config_exceptions.append(exc_info)
|
||||||
|
return IntegrationConfigInfo(None, config_exceptions)
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
exc_info = ConfigExceptionInfo(
|
||||||
|
exc,
|
||||||
|
ConfigErrorTranslationKey.CONFIG_SCHEMA_UNKNOWN_ERR,
|
||||||
|
domain,
|
||||||
|
config,
|
||||||
|
integration_docs,
|
||||||
|
)
|
||||||
|
config_exceptions.append(exc_info)
|
||||||
|
return IntegrationConfigInfo(None, config_exceptions)
|
||||||
|
|
||||||
component_platform_schema = getattr(
|
component_platform_schema = getattr(
|
||||||
component, "PLATFORM_SCHEMA_BASE", getattr(component, "PLATFORM_SCHEMA", None)
|
component, "PLATFORM_SCHEMA_BASE", getattr(component, "PLATFORM_SCHEMA", None)
|
||||||
)
|
)
|
||||||
|
|
||||||
if component_platform_schema is None:
|
if component_platform_schema is None:
|
||||||
return config
|
return IntegrationConfigInfo(config, [])
|
||||||
|
|
||||||
platforms = []
|
platforms: list[ConfigType] = []
|
||||||
for p_name, p_config in config_per_platform(config, domain):
|
for p_name, p_config in config_per_platform(config, domain):
|
||||||
# Validate component specific platform schema
|
# Validate component specific platform schema
|
||||||
|
platform_name = f"{domain}.{p_name}"
|
||||||
try:
|
try:
|
||||||
p_validated = component_platform_schema(p_config)
|
p_validated = component_platform_schema(p_config)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as exc:
|
||||||
async_log_schema_error(
|
exc_info = ConfigExceptionInfo(
|
||||||
ex, domain, p_config, hass, integration.documentation
|
exc,
|
||||||
)
|
ConfigErrorTranslationKey.PLATFORM_CONFIG_VALIDATION_ERR,
|
||||||
continue
|
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
_LOGGER.exception(
|
|
||||||
(
|
|
||||||
"Unknown error validating %s platform config with %s component"
|
|
||||||
" platform schema"
|
|
||||||
),
|
|
||||||
p_name,
|
|
||||||
domain,
|
domain,
|
||||||
|
p_config,
|
||||||
|
integration_docs,
|
||||||
)
|
)
|
||||||
|
config_exceptions.append(exc_info)
|
||||||
|
continue
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
exc_info = ConfigExceptionInfo(
|
||||||
|
exc,
|
||||||
|
ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR,
|
||||||
|
str(p_name),
|
||||||
|
config,
|
||||||
|
integration_docs,
|
||||||
|
)
|
||||||
|
config_exceptions.append(exc_info)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Not all platform components follow same pattern for platforms
|
# Not all platform components follow same pattern for platforms
|
||||||
|
@ -1117,38 +1370,53 @@ async def async_process_component_config( # noqa: C901
|
||||||
|
|
||||||
try:
|
try:
|
||||||
p_integration = await async_get_integration_with_requirements(hass, p_name)
|
p_integration = await async_get_integration_with_requirements(hass, p_name)
|
||||||
except (RequirementsNotFound, IntegrationNotFound) as ex:
|
except (RequirementsNotFound, IntegrationNotFound) as exc:
|
||||||
_LOGGER.error("Platform error: %s - %s", domain, ex)
|
exc_info = ConfigExceptionInfo(
|
||||||
|
exc,
|
||||||
|
ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_ERR,
|
||||||
|
platform_name,
|
||||||
|
p_config,
|
||||||
|
integration_docs,
|
||||||
|
)
|
||||||
|
config_exceptions.append(exc_info)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
platform = p_integration.get_platform(domain)
|
platform = p_integration.get_platform(domain)
|
||||||
except LOAD_EXCEPTIONS:
|
except LOAD_EXCEPTIONS as exc:
|
||||||
_LOGGER.exception("Platform error: %s", domain)
|
exc_info = ConfigExceptionInfo(
|
||||||
|
exc,
|
||||||
|
ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC,
|
||||||
|
platform_name,
|
||||||
|
p_config,
|
||||||
|
integration_docs,
|
||||||
|
)
|
||||||
|
config_exceptions.append(exc_info)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Validate platform specific schema
|
# Validate platform specific schema
|
||||||
if hasattr(platform, "PLATFORM_SCHEMA"):
|
if hasattr(platform, "PLATFORM_SCHEMA"):
|
||||||
try:
|
try:
|
||||||
p_validated = platform.PLATFORM_SCHEMA(p_config)
|
p_validated = platform.PLATFORM_SCHEMA(p_config)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as exc:
|
||||||
async_log_schema_error(
|
exc_info = ConfigExceptionInfo(
|
||||||
ex,
|
exc,
|
||||||
f"{domain}.{p_name}",
|
ConfigErrorTranslationKey.PLATFORM_CONFIG_VALIDATION_ERR,
|
||||||
|
platform_name,
|
||||||
p_config,
|
p_config,
|
||||||
hass,
|
|
||||||
p_integration.documentation,
|
p_integration.documentation,
|
||||||
)
|
)
|
||||||
|
config_exceptions.append(exc_info)
|
||||||
continue
|
continue
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
_LOGGER.exception(
|
exc_info = ConfigExceptionInfo(
|
||||||
(
|
exc,
|
||||||
"Unknown error validating config for %s platform for %s"
|
ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR,
|
||||||
" component with PLATFORM_SCHEMA"
|
|
||||||
),
|
|
||||||
p_name,
|
p_name,
|
||||||
domain,
|
p_config,
|
||||||
|
p_integration.documentation,
|
||||||
)
|
)
|
||||||
|
config_exceptions.append(exc_info)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
platforms.append(p_validated)
|
platforms.append(p_validated)
|
||||||
|
@ -1158,7 +1426,7 @@ async def async_process_component_config( # noqa: C901
|
||||||
config = config_without_domain(config, domain)
|
config = config_without_domain(config, domain)
|
||||||
config[domain] = platforms
|
config[domain] = platforms
|
||||||
|
|
||||||
return config
|
return IntegrationConfigInfo(config, config_exceptions)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -1183,36 +1451,6 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> str | None:
|
||||||
return res.error_str
|
return res.error_str
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_notify_setup_error(
|
|
||||||
hass: HomeAssistant, component: str, display_link: str | None = None
|
|
||||||
) -> None:
|
|
||||||
"""Print a persistent notification.
|
|
||||||
|
|
||||||
This method must be run in the event loop.
|
|
||||||
"""
|
|
||||||
# pylint: disable-next=import-outside-toplevel
|
|
||||||
from .components import persistent_notification
|
|
||||||
|
|
||||||
if (errors := hass.data.get(DATA_PERSISTENT_ERRORS)) is None:
|
|
||||||
errors = hass.data[DATA_PERSISTENT_ERRORS] = {}
|
|
||||||
|
|
||||||
errors[component] = errors.get(component) or display_link
|
|
||||||
|
|
||||||
message = "The following integrations and platforms could not be set up:\n\n"
|
|
||||||
|
|
||||||
for name, link in errors.items():
|
|
||||||
show_logs = f"[Show logs](/config/logs?filter={name})"
|
|
||||||
part = f"[{name}]({link})" if link else name
|
|
||||||
message += f" - {part} ({show_logs})\n"
|
|
||||||
|
|
||||||
message += "\nPlease check your config and [logs](/config/logs)."
|
|
||||||
|
|
||||||
persistent_notification.async_create(
|
|
||||||
hass, message, "Invalid config", "invalid_config"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def safe_mode_enabled(config_dir: str) -> bool:
|
def safe_mode_enabled(config_dir: str) -> bool:
|
||||||
"""Return if safe mode is enabled.
|
"""Return if safe mode is enabled.
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,31 @@ class HomeAssistantError(Exception):
|
||||||
self.translation_placeholders = translation_placeholders
|
self.translation_placeholders = translation_placeholders
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigValidationError(HomeAssistantError, ExceptionGroup[Exception]):
|
||||||
|
"""A validation exception occurred when validating the configuration."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
exceptions: list[Exception],
|
||||||
|
translation_domain: str | None = None,
|
||||||
|
translation_key: str | None = None,
|
||||||
|
translation_placeholders: dict[str, str] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize exception."""
|
||||||
|
super().__init__(
|
||||||
|
*(message, exceptions),
|
||||||
|
translation_domain=translation_domain,
|
||||||
|
translation_key=translation_key,
|
||||||
|
translation_placeholders=translation_placeholders,
|
||||||
|
)
|
||||||
|
self._message = message
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return exception message string."""
|
||||||
|
return self._message
|
||||||
|
|
||||||
|
|
||||||
class ServiceValidationError(HomeAssistantError):
|
class ServiceValidationError(HomeAssistantError):
|
||||||
"""A validation exception occurred when calling a service."""
|
"""A validation exception occurred when calling a service."""
|
||||||
|
|
||||||
|
|
|
@ -355,7 +355,7 @@ class EntityComponent(Generic[_EntityT]):
|
||||||
|
|
||||||
integration = await async_get_integration(self.hass, self.domain)
|
integration = await async_get_integration(self.hass, self.domain)
|
||||||
|
|
||||||
processed_conf = await conf_util.async_process_component_config(
|
processed_conf = await conf_util.async_process_component_and_handle_errors(
|
||||||
self.hass, conf, integration
|
self.hass, conf, integration
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, Literal, overload
|
||||||
|
|
||||||
from homeassistant import config as conf_util
|
from homeassistant import config as conf_util
|
||||||
from homeassistant.const import SERVICE_RELOAD
|
from homeassistant.const import SERVICE_RELOAD
|
||||||
|
@ -60,7 +60,7 @@ async def _resetup_platform(
|
||||||
"""Resetup a platform."""
|
"""Resetup a platform."""
|
||||||
integration = await async_get_integration(hass, platform_domain)
|
integration = await async_get_integration(hass, platform_domain)
|
||||||
|
|
||||||
conf = await conf_util.async_process_component_config(
|
conf = await conf_util.async_process_component_and_handle_errors(
|
||||||
hass, unprocessed_config, integration
|
hass, unprocessed_config, integration
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -136,14 +136,41 @@ async def _async_reconfig_platform(
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
async def async_integration_yaml_config(
|
async def async_integration_yaml_config(
|
||||||
hass: HomeAssistant, integration_name: str
|
hass: HomeAssistant, integration_name: str
|
||||||
|
) -> ConfigType | None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def async_integration_yaml_config(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
integration_name: str,
|
||||||
|
*,
|
||||||
|
raise_on_failure: Literal[True],
|
||||||
|
) -> ConfigType:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
async def async_integration_yaml_config(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
integration_name: str,
|
||||||
|
*,
|
||||||
|
raise_on_failure: Literal[False] | bool,
|
||||||
|
) -> ConfigType | None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
async def async_integration_yaml_config(
|
||||||
|
hass: HomeAssistant, integration_name: str, *, raise_on_failure: bool = False
|
||||||
) -> ConfigType | None:
|
) -> ConfigType | None:
|
||||||
"""Fetch the latest yaml configuration for an integration."""
|
"""Fetch the latest yaml configuration for an integration."""
|
||||||
integration = await async_get_integration(hass, integration_name)
|
integration = await async_get_integration(hass, integration_name)
|
||||||
|
config = await conf_util.async_hass_config_yaml(hass)
|
||||||
return await conf_util.async_process_component_config(
|
return await conf_util.async_process_component_and_handle_errors(
|
||||||
hass, await conf_util.async_hass_config_yaml(hass), integration
|
hass, config, integration, raise_on_failure=raise_on_failure
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,14 +11,13 @@ from types import ModuleType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from . import config as conf_util, core, loader, requirements
|
from . import config as conf_util, core, loader, requirements
|
||||||
from .config import async_notify_setup_error
|
|
||||||
from .const import (
|
from .const import (
|
||||||
EVENT_COMPONENT_LOADED,
|
EVENT_COMPONENT_LOADED,
|
||||||
EVENT_HOMEASSISTANT_START,
|
EVENT_HOMEASSISTANT_START,
|
||||||
PLATFORM_FORMAT,
|
PLATFORM_FORMAT,
|
||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from .core import CALLBACK_TYPE, DOMAIN as HOMEASSISTANT_DOMAIN
|
from .core import CALLBACK_TYPE, DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
|
||||||
from .exceptions import DependencyError, HomeAssistantError
|
from .exceptions import DependencyError, HomeAssistantError
|
||||||
from .helpers.issue_registry import IssueSeverity, async_create_issue
|
from .helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from .helpers.typing import ConfigType
|
from .helpers.typing import ConfigType
|
||||||
|
@ -56,10 +55,47 @@ DATA_SETUP_TIME = "setup_time"
|
||||||
|
|
||||||
DATA_DEPS_REQS = "deps_reqs_processed"
|
DATA_DEPS_REQS = "deps_reqs_processed"
|
||||||
|
|
||||||
|
DATA_PERSISTENT_ERRORS = "bootstrap_persistent_errors"
|
||||||
|
|
||||||
|
NOTIFY_FOR_TRANSLATION_KEYS = [
|
||||||
|
"config_validation_err",
|
||||||
|
"platform_config_validation_err",
|
||||||
|
]
|
||||||
|
|
||||||
SLOW_SETUP_WARNING = 10
|
SLOW_SETUP_WARNING = 10
|
||||||
SLOW_SETUP_MAX_WAIT = 300
|
SLOW_SETUP_MAX_WAIT = 300
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_notify_setup_error(
|
||||||
|
hass: HomeAssistant, component: str, display_link: str | None = None
|
||||||
|
) -> None:
|
||||||
|
"""Print a persistent notification.
|
||||||
|
|
||||||
|
This method must be run in the event loop.
|
||||||
|
"""
|
||||||
|
# pylint: disable-next=import-outside-toplevel
|
||||||
|
from .components import persistent_notification
|
||||||
|
|
||||||
|
if (errors := hass.data.get(DATA_PERSISTENT_ERRORS)) is None:
|
||||||
|
errors = hass.data[DATA_PERSISTENT_ERRORS] = {}
|
||||||
|
|
||||||
|
errors[component] = errors.get(component) or display_link
|
||||||
|
|
||||||
|
message = "The following integrations and platforms could not be set up:\n\n"
|
||||||
|
|
||||||
|
for name, link in errors.items():
|
||||||
|
show_logs = f"[Show logs](/config/logs?filter={name})"
|
||||||
|
part = f"[{name}]({link})" if link else name
|
||||||
|
message += f" - {part} ({show_logs})\n"
|
||||||
|
|
||||||
|
message += "\nPlease check your config and [logs](/config/logs)."
|
||||||
|
|
||||||
|
persistent_notification.async_create(
|
||||||
|
hass, message, "Invalid config", "invalid_config"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@core.callback
|
@core.callback
|
||||||
def async_set_domains_to_be_loaded(hass: core.HomeAssistant, domains: set[str]) -> None:
|
def async_set_domains_to_be_loaded(hass: core.HomeAssistant, domains: set[str]) -> None:
|
||||||
"""Set domains that are going to be loaded from the config.
|
"""Set domains that are going to be loaded from the config.
|
||||||
|
@ -217,10 +253,18 @@ async def _async_setup_component(
|
||||||
log_error(f"Unable to import component: {err}", err)
|
log_error(f"Unable to import component: {err}", err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
processed_config = await conf_util.async_process_component_config(
|
integration_config_info = await conf_util.async_process_component_config(
|
||||||
hass, config, integration
|
hass, config, integration
|
||||||
)
|
)
|
||||||
|
processed_config = conf_util.async_handle_component_errors(
|
||||||
|
hass, integration_config_info, integration
|
||||||
|
)
|
||||||
|
for platform_exception in integration_config_info.exception_info_list:
|
||||||
|
if platform_exception.translation_key not in NOTIFY_FOR_TRANSLATION_KEYS:
|
||||||
|
continue
|
||||||
|
async_notify_setup_error(
|
||||||
|
hass, platform_exception.platform_name, platform_exception.integration_link
|
||||||
|
)
|
||||||
if processed_config is None:
|
if processed_config is None:
|
||||||
log_error("Invalid config.")
|
log_error("Invalid config.")
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -984,7 +984,10 @@ def assert_setup_component(count, domain=None):
|
||||||
async def mock_psc(hass, config_input, integration):
|
async def mock_psc(hass, config_input, integration):
|
||||||
"""Mock the prepare_setup_component to capture config."""
|
"""Mock the prepare_setup_component to capture config."""
|
||||||
domain_input = integration.domain
|
domain_input = integration.domain
|
||||||
res = await async_process_component_config(hass, config_input, integration)
|
integration_config_info = await async_process_component_config(
|
||||||
|
hass, config_input, integration
|
||||||
|
)
|
||||||
|
res = integration_config_info.config
|
||||||
config[domain_input] = None if res is None else res.get(domain_input)
|
config[domain_input] = None if res is None else res.get(domain_input)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Configuration for %s, Validated: %s, Original %s",
|
"Configuration for %s, Validated: %s, Original %s",
|
||||||
|
@ -992,7 +995,7 @@ def assert_setup_component(count, domain=None):
|
||||||
config[domain_input],
|
config[domain_input],
|
||||||
config_input.get(domain_input),
|
config_input.get(domain_input),
|
||||||
)
|
)
|
||||||
return res
|
return integration_config_info
|
||||||
|
|
||||||
assert isinstance(config, dict)
|
assert isinstance(config, dict)
|
||||||
with patch("homeassistant.config.async_process_component_config", mock_psc):
|
with patch("homeassistant.config.async_process_component_config", mock_psc):
|
||||||
|
|
|
@ -3,10 +3,12 @@ import logging
|
||||||
from unittest.mock import AsyncMock, Mock, patch
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config
|
from homeassistant import config
|
||||||
from homeassistant.const import SERVICE_RELOAD
|
from homeassistant.const import SERVICE_RELOAD
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigValidationError, HomeAssistantError
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.entity_platform import async_get_platforms
|
from homeassistant.helpers.entity_platform import async_get_platforms
|
||||||
from homeassistant.helpers.reload import (
|
from homeassistant.helpers.reload import (
|
||||||
|
@ -139,7 +141,9 @@ async def test_setup_reload_service_when_async_process_component_config_fails(
|
||||||
|
|
||||||
yaml_path = get_fixture_path("helpers/reload_configuration.yaml")
|
yaml_path = get_fixture_path("helpers/reload_configuration.yaml")
|
||||||
with patch.object(config, "YAML_CONFIG_FILE", yaml_path), patch.object(
|
with patch.object(config, "YAML_CONFIG_FILE", yaml_path), patch.object(
|
||||||
config, "async_process_component_config", return_value=None
|
config,
|
||||||
|
"async_process_component_config",
|
||||||
|
return_value=config.IntegrationConfigInfo(None, []),
|
||||||
):
|
):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
PLATFORM,
|
PLATFORM,
|
||||||
|
@ -208,8 +212,49 @@ async def test_async_integration_yaml_config(hass: HomeAssistant) -> None:
|
||||||
yaml_path = get_fixture_path(f"helpers/{DOMAIN}_configuration.yaml")
|
yaml_path = get_fixture_path(f"helpers/{DOMAIN}_configuration.yaml")
|
||||||
with patch.object(config, "YAML_CONFIG_FILE", yaml_path):
|
with patch.object(config, "YAML_CONFIG_FILE", yaml_path):
|
||||||
processed_config = await async_integration_yaml_config(hass, DOMAIN)
|
processed_config = await async_integration_yaml_config(hass, DOMAIN)
|
||||||
|
assert processed_config == {DOMAIN: [{"name": "one"}, {"name": "two"}]}
|
||||||
|
# Test fetching yaml config does not raise when the raise_on_failure option is set
|
||||||
|
processed_config = await async_integration_yaml_config(
|
||||||
|
hass, DOMAIN, raise_on_failure=True
|
||||||
|
)
|
||||||
|
assert processed_config == {DOMAIN: [{"name": "one"}, {"name": "two"}]}
|
||||||
|
|
||||||
assert processed_config == {DOMAIN: [{"name": "one"}, {"name": "two"}]}
|
|
||||||
|
async def test_async_integration_failing_yaml_config(hass: HomeAssistant) -> None:
|
||||||
|
"""Test reloading yaml config for an integration fails.
|
||||||
|
|
||||||
|
In case an integration reloads its yaml configuration it should throw when
|
||||||
|
the new config failed to load and raise_on_failure is set to True.
|
||||||
|
"""
|
||||||
|
schema_without_name_attr = vol.Schema({vol.Required("some_option"): str})
|
||||||
|
|
||||||
|
mock_integration(hass, MockModule(DOMAIN, config_schema=schema_without_name_attr))
|
||||||
|
|
||||||
|
yaml_path = get_fixture_path(f"helpers/{DOMAIN}_configuration.yaml")
|
||||||
|
with patch.object(config, "YAML_CONFIG_FILE", yaml_path):
|
||||||
|
# Test fetching yaml config does not raise without raise_on_failure option
|
||||||
|
processed_config = await async_integration_yaml_config(hass, DOMAIN)
|
||||||
|
assert processed_config is None
|
||||||
|
# Test fetching yaml config does not raise when the raise_on_failure option is set
|
||||||
|
with pytest.raises(ConfigValidationError):
|
||||||
|
await async_integration_yaml_config(hass, DOMAIN, raise_on_failure=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_integration_failing_on_reload(hass: HomeAssistant) -> None:
|
||||||
|
"""Test reloading yaml config for an integration fails with an other exception.
|
||||||
|
|
||||||
|
In case an integration reloads its yaml configuration it should throw when
|
||||||
|
the new config failed to load and raise_on_failure is set to True.
|
||||||
|
"""
|
||||||
|
mock_integration(hass, MockModule(DOMAIN))
|
||||||
|
|
||||||
|
yaml_path = get_fixture_path(f"helpers/{DOMAIN}_configuration.yaml")
|
||||||
|
with patch.object(config, "YAML_CONFIG_FILE", yaml_path), patch(
|
||||||
|
"homeassistant.config.async_process_component_config",
|
||||||
|
side_effect=HomeAssistantError(),
|
||||||
|
), pytest.raises(HomeAssistantError):
|
||||||
|
# Test fetching yaml config does raise when the raise_on_failure option is set
|
||||||
|
await async_integration_yaml_config(hass, DOMAIN, raise_on_failure=True)
|
||||||
|
|
||||||
|
|
||||||
async def test_async_integration_missing_yaml_config(hass: HomeAssistant) -> None:
|
async def test_async_integration_missing_yaml_config(hass: HomeAssistant) -> None:
|
||||||
|
|
|
@ -1013,7 +1013,10 @@ async def test_bootstrap_dependencies(
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.setup.loader.async_get_integrations",
|
"homeassistant.setup.loader.async_get_integrations",
|
||||||
side_effect=mock_async_get_integrations,
|
side_effect=mock_async_get_integrations,
|
||||||
), patch("homeassistant.config.async_process_component_config", return_value={}):
|
), patch(
|
||||||
|
"homeassistant.config.async_process_component_config",
|
||||||
|
return_value=config_util.IntegrationConfigInfo({}, []),
|
||||||
|
):
|
||||||
bootstrap.async_set_domains_to_be_loaded(hass, {integration})
|
bootstrap.async_set_domains_to_be_loaded(hass, {integration})
|
||||||
await bootstrap.async_setup_multi_components(hass, {integration}, {})
|
await bootstrap.async_setup_multi_components(hass, {integration}, {})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
|
@ -30,6 +30,7 @@ from homeassistant.const import (
|
||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
from homeassistant.core import ConfigSource, HomeAssistant, HomeAssistantError
|
from homeassistant.core import ConfigSource, HomeAssistant, HomeAssistantError
|
||||||
|
from homeassistant.exceptions import ConfigValidationError
|
||||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||||
import homeassistant.helpers.check_config as check_config
|
import homeassistant.helpers.check_config as check_config
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
@ -1427,71 +1428,132 @@ async def test_component_config_exceptions(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test unexpected exceptions validating component config."""
|
"""Test unexpected exceptions validating component config."""
|
||||||
# Config validator
|
# Config validator
|
||||||
|
test_integration = Mock(
|
||||||
|
domain="test_domain",
|
||||||
|
get_platform=Mock(
|
||||||
|
return_value=Mock(
|
||||||
|
async_validate_config=AsyncMock(side_effect=ValueError("broken"))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
assert (
|
assert (
|
||||||
await config_util.async_process_component_config(
|
await config_util.async_process_component_and_handle_errors(
|
||||||
hass,
|
hass, {}, integration=test_integration
|
||||||
{},
|
|
||||||
integration=Mock(
|
|
||||||
domain="test_domain",
|
|
||||||
get_platform=Mock(
|
|
||||||
return_value=Mock(
|
|
||||||
async_validate_config=AsyncMock(
|
|
||||||
side_effect=ValueError("broken")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
assert "ValueError: broken" in caplog.text
|
assert "ValueError: broken" in caplog.text
|
||||||
assert "Unknown error calling test_domain config validator" in caplog.text
|
assert "Unknown error calling test_domain config validator" in caplog.text
|
||||||
|
caplog.clear()
|
||||||
|
with pytest.raises(HomeAssistantError) as ex:
|
||||||
|
await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass, {}, integration=test_integration, raise_on_failure=True
|
||||||
|
)
|
||||||
|
assert "ValueError: broken" in caplog.text
|
||||||
|
assert "Unknown error calling test_domain config validator" in caplog.text
|
||||||
|
assert str(ex.value) == "Unknown error calling test_domain config validator"
|
||||||
|
|
||||||
# component.CONFIG_SCHEMA
|
test_integration = Mock(
|
||||||
|
domain="test_domain",
|
||||||
|
get_platform=Mock(
|
||||||
|
return_value=Mock(
|
||||||
|
async_validate_config=AsyncMock(
|
||||||
|
side_effect=HomeAssistantError("broken")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])),
|
||||||
|
)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
assert (
|
assert (
|
||||||
await config_util.async_process_component_config(
|
await config_util.async_process_component_and_handle_errors(
|
||||||
hass,
|
hass, {}, integration=test_integration, raise_on_failure=False
|
||||||
{},
|
)
|
||||||
integration=Mock(
|
is None
|
||||||
domain="test_domain",
|
)
|
||||||
get_platform=Mock(return_value=None),
|
assert "Invalid config for 'test_domain': broken" in caplog.text
|
||||||
get_component=Mock(
|
with pytest.raises(HomeAssistantError) as ex:
|
||||||
return_value=Mock(
|
await config_util.async_process_component_and_handle_errors(
|
||||||
CONFIG_SCHEMA=Mock(side_effect=ValueError("broken"))
|
hass, {}, integration=test_integration, raise_on_failure=True
|
||||||
)
|
)
|
||||||
),
|
assert "Invalid config for 'test_domain': broken" in str(ex.value)
|
||||||
),
|
|
||||||
|
# component.CONFIG_SCHEMA
|
||||||
|
caplog.clear()
|
||||||
|
test_integration = Mock(
|
||||||
|
domain="test_domain",
|
||||||
|
get_platform=Mock(return_value=None),
|
||||||
|
get_component=Mock(
|
||||||
|
return_value=Mock(CONFIG_SCHEMA=Mock(side_effect=ValueError("broken")))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass,
|
||||||
|
{},
|
||||||
|
integration=test_integration,
|
||||||
|
raise_on_failure=False,
|
||||||
)
|
)
|
||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
assert "ValueError: broken" in caplog.text
|
|
||||||
assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text
|
assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text
|
||||||
|
with pytest.raises(HomeAssistantError) as ex:
|
||||||
|
await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass,
|
||||||
|
{},
|
||||||
|
integration=test_integration,
|
||||||
|
raise_on_failure=True,
|
||||||
|
)
|
||||||
|
assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text
|
||||||
|
assert str(ex.value) == "Unknown error calling test_domain CONFIG_SCHEMA"
|
||||||
|
|
||||||
# component.PLATFORM_SCHEMA
|
# component.PLATFORM_SCHEMA
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
assert await config_util.async_process_component_config(
|
test_integration = Mock(
|
||||||
|
domain="test_domain",
|
||||||
|
get_platform=Mock(return_value=None),
|
||||||
|
get_component=Mock(
|
||||||
|
return_value=Mock(
|
||||||
|
spec=["PLATFORM_SCHEMA_BASE"],
|
||||||
|
PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert await config_util.async_process_component_and_handle_errors(
|
||||||
hass,
|
hass,
|
||||||
{"test_domain": {"platform": "test_platform"}},
|
{"test_domain": {"platform": "test_platform"}},
|
||||||
integration=Mock(
|
integration=test_integration,
|
||||||
domain="test_domain",
|
raise_on_failure=False,
|
||||||
get_platform=Mock(return_value=None),
|
|
||||||
get_component=Mock(
|
|
||||||
return_value=Mock(
|
|
||||||
spec=["PLATFORM_SCHEMA_BASE"],
|
|
||||||
PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
) == {"test_domain": []}
|
) == {"test_domain": []}
|
||||||
assert "ValueError: broken" in caplog.text
|
assert "ValueError: broken" in caplog.text
|
||||||
assert (
|
assert (
|
||||||
"Unknown error validating test_platform platform config "
|
"Unknown error validating config for test_platform platform "
|
||||||
"with test_domain component platform schema"
|
"for test_domain component with PLATFORM_SCHEMA"
|
||||||
) in caplog.text
|
) in caplog.text
|
||||||
|
caplog.clear()
|
||||||
|
with pytest.raises(HomeAssistantError) as ex:
|
||||||
|
await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass,
|
||||||
|
{"test_domain": {"platform": "test_platform"}},
|
||||||
|
integration=test_integration,
|
||||||
|
raise_on_failure=True,
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
"Unknown error validating config for test_platform platform "
|
||||||
|
"for test_domain component with PLATFORM_SCHEMA"
|
||||||
|
) in caplog.text
|
||||||
|
assert str(ex.value) == (
|
||||||
|
"Unknown error validating config for test_platform platform "
|
||||||
|
"for test_domain component with PLATFORM_SCHEMA"
|
||||||
|
)
|
||||||
|
|
||||||
# platform.PLATFORM_SCHEMA
|
# platform.PLATFORM_SCHEMA
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
test_integration = Mock(
|
||||||
|
domain="test_domain",
|
||||||
|
get_platform=Mock(return_value=None),
|
||||||
|
get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])),
|
||||||
|
)
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.config.async_get_integration_with_requirements",
|
"homeassistant.config.async_get_integration_with_requirements",
|
||||||
return_value=Mock( # integration that owns platform
|
return_value=Mock( # integration that owns platform
|
||||||
|
@ -1502,67 +1564,337 @@ async def test_component_config_exceptions(
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
assert await config_util.async_process_component_config(
|
assert await config_util.async_process_component_and_handle_errors(
|
||||||
hass,
|
hass,
|
||||||
{"test_domain": {"platform": "test_platform"}},
|
{"test_domain": {"platform": "test_platform"}},
|
||||||
integration=Mock(
|
integration=test_integration,
|
||||||
domain="test_domain",
|
raise_on_failure=False,
|
||||||
get_platform=Mock(return_value=None),
|
|
||||||
get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])),
|
|
||||||
),
|
|
||||||
) == {"test_domain": []}
|
) == {"test_domain": []}
|
||||||
assert "ValueError: broken" in caplog.text
|
assert "ValueError: broken" in caplog.text
|
||||||
|
assert (
|
||||||
|
"Unknown error validating config for test_platform platform for test_domain"
|
||||||
|
" component with PLATFORM_SCHEMA"
|
||||||
|
) in caplog.text
|
||||||
|
caplog.clear()
|
||||||
|
with pytest.raises(HomeAssistantError) as ex:
|
||||||
|
assert await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass,
|
||||||
|
{"test_domain": {"platform": "test_platform"}},
|
||||||
|
integration=test_integration,
|
||||||
|
raise_on_failure=True,
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
"Unknown error validating config for test_platform platform for test_domain"
|
||||||
|
" component with PLATFORM_SCHEMA"
|
||||||
|
) in str(ex.value)
|
||||||
|
assert "ValueError: broken" in caplog.text
|
||||||
assert (
|
assert (
|
||||||
"Unknown error validating config for test_platform platform for test_domain"
|
"Unknown error validating config for test_platform platform for test_domain"
|
||||||
" component with PLATFORM_SCHEMA" in caplog.text
|
" component with PLATFORM_SCHEMA" in caplog.text
|
||||||
)
|
)
|
||||||
|
# Test multiple platform failures
|
||||||
|
assert await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"test_domain": [
|
||||||
|
{"platform": "test_platform1"},
|
||||||
|
{"platform": "test_platform2"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
integration=test_integration,
|
||||||
|
raise_on_failure=False,
|
||||||
|
) == {"test_domain": []}
|
||||||
|
assert "ValueError: broken" in caplog.text
|
||||||
|
assert (
|
||||||
|
"Unknown error validating config for test_platform1 platform "
|
||||||
|
"for test_domain component with PLATFORM_SCHEMA"
|
||||||
|
) in caplog.text
|
||||||
|
assert (
|
||||||
|
"Unknown error validating config for test_platform2 platform "
|
||||||
|
"for test_domain component with PLATFORM_SCHEMA"
|
||||||
|
) in caplog.text
|
||||||
|
caplog.clear()
|
||||||
|
with pytest.raises(HomeAssistantError) as ex:
|
||||||
|
assert await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"test_domain": [
|
||||||
|
{"platform": "test_platform1"},
|
||||||
|
{"platform": "test_platform2"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
integration=test_integration,
|
||||||
|
raise_on_failure=True,
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
"Failed to process component config for integration test_domain"
|
||||||
|
" due to multiple errors (2), check the logs for more information."
|
||||||
|
) in str(ex.value)
|
||||||
|
assert "ValueError: broken" in caplog.text
|
||||||
|
assert (
|
||||||
|
"Unknown error validating config for test_platform1 platform "
|
||||||
|
"for test_domain component with PLATFORM_SCHEMA"
|
||||||
|
) in caplog.text
|
||||||
|
assert (
|
||||||
|
"Unknown error validating config for test_platform2 platform "
|
||||||
|
"for test_domain component with PLATFORM_SCHEMA"
|
||||||
|
) in caplog.text
|
||||||
|
|
||||||
|
# get_platform("domain") raising on ImportError
|
||||||
|
caplog.clear()
|
||||||
|
test_integration = Mock(
|
||||||
|
domain="test_domain",
|
||||||
|
get_platform=Mock(return_value=None),
|
||||||
|
get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])),
|
||||||
|
)
|
||||||
|
import_error = ImportError(
|
||||||
|
("ModuleNotFoundError: No module named 'not_installed_something'"),
|
||||||
|
name="not_installed_something",
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.config.async_get_integration_with_requirements",
|
||||||
|
return_value=Mock( # integration that owns platform
|
||||||
|
get_platform=Mock(side_effect=import_error)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
assert await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass,
|
||||||
|
{"test_domain": {"platform": "test_platform"}},
|
||||||
|
integration=test_integration,
|
||||||
|
raise_on_failure=False,
|
||||||
|
) == {"test_domain": []}
|
||||||
|
assert (
|
||||||
|
"ImportError: ModuleNotFoundError: No module named "
|
||||||
|
"'not_installed_something'" in caplog.text
|
||||||
|
)
|
||||||
|
caplog.clear()
|
||||||
|
with pytest.raises(HomeAssistantError) as ex:
|
||||||
|
assert await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass,
|
||||||
|
{"test_domain": {"platform": "test_platform"}},
|
||||||
|
integration=test_integration,
|
||||||
|
raise_on_failure=True,
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
"ImportError: ModuleNotFoundError: No module named "
|
||||||
|
"'not_installed_something'" in caplog.text
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
"Platform error: test_domain - ModuleNotFoundError: "
|
||||||
|
"No module named 'not_installed_something'"
|
||||||
|
) in caplog.text
|
||||||
|
assert (
|
||||||
|
"Platform error: test_domain - ModuleNotFoundError: "
|
||||||
|
"No module named 'not_installed_something'"
|
||||||
|
) in str(ex.value)
|
||||||
|
|
||||||
# get_platform("config") raising
|
# get_platform("config") raising
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
test_integration = Mock(
|
||||||
|
pkg_path="homeassistant.components.test_domain",
|
||||||
|
domain="test_domain",
|
||||||
|
get_platform=Mock(
|
||||||
|
side_effect=ImportError(
|
||||||
|
("ModuleNotFoundError: No module named 'not_installed_something'"),
|
||||||
|
name="not_installed_something",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
assert (
|
assert (
|
||||||
await config_util.async_process_component_config(
|
await config_util.async_process_component_and_handle_errors(
|
||||||
hass,
|
hass,
|
||||||
{"test_domain": {}},
|
{"test_domain": {}},
|
||||||
integration=Mock(
|
integration=test_integration,
|
||||||
pkg_path="homeassistant.components.test_domain",
|
raise_on_failure=False,
|
||||||
domain="test_domain",
|
|
||||||
get_platform=Mock(
|
|
||||||
side_effect=ImportError(
|
|
||||||
(
|
|
||||||
"ModuleNotFoundError: No module named"
|
|
||||||
" 'not_installed_something'"
|
|
||||||
),
|
|
||||||
name="not_installed_something",
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
"Error importing config platform test_domain: ModuleNotFoundError: No module"
|
"Error importing config platform test_domain: ModuleNotFoundError: "
|
||||||
" named 'not_installed_something'" in caplog.text
|
"No module named 'not_installed_something'" in caplog.text
|
||||||
|
)
|
||||||
|
with pytest.raises(HomeAssistantError) as ex:
|
||||||
|
await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass,
|
||||||
|
{"test_domain": {}},
|
||||||
|
integration=test_integration,
|
||||||
|
raise_on_failure=True,
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
"Error importing config platform test_domain: ModuleNotFoundError: "
|
||||||
|
"No module named 'not_installed_something'" in caplog.text
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
"Error importing config platform test_domain: ModuleNotFoundError: "
|
||||||
|
"No module named 'not_installed_something'" in str(ex.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
# get_component raising
|
# get_component raising
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
test_integration = Mock(
|
||||||
|
pkg_path="homeassistant.components.test_domain",
|
||||||
|
domain="test_domain",
|
||||||
|
get_component=Mock(
|
||||||
|
side_effect=FileNotFoundError("No such file or directory: b'liblibc.a'")
|
||||||
|
),
|
||||||
|
)
|
||||||
assert (
|
assert (
|
||||||
await config_util.async_process_component_config(
|
await config_util.async_process_component_and_handle_errors(
|
||||||
hass,
|
hass,
|
||||||
{"test_domain": {}},
|
{"test_domain": {}},
|
||||||
integration=Mock(
|
integration=test_integration,
|
||||||
pkg_path="homeassistant.components.test_domain",
|
raise_on_failure=False,
|
||||||
domain="test_domain",
|
|
||||||
get_component=Mock(
|
|
||||||
side_effect=FileNotFoundError(
|
|
||||||
"No such file or directory: b'liblibc.a'"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
assert "Unable to import test_domain: No such file or directory" in caplog.text
|
assert "Unable to import test_domain: No such file or directory" in caplog.text
|
||||||
|
with pytest.raises(HomeAssistantError) as ex:
|
||||||
|
await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass,
|
||||||
|
{"test_domain": {}},
|
||||||
|
integration=test_integration,
|
||||||
|
raise_on_failure=True,
|
||||||
|
)
|
||||||
|
assert "Unable to import test_domain: No such file or directory" in caplog.text
|
||||||
|
assert "Unable to import test_domain: No such file or directory" in str(ex.value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception_info_list", "error", "messages", "show_stack_trace", "translation_key"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
[
|
||||||
|
config_util.ConfigExceptionInfo(
|
||||||
|
ImportError("bla"),
|
||||||
|
"component_import_err",
|
||||||
|
"test_domain",
|
||||||
|
{"test_domain": []},
|
||||||
|
"https://example.com",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
"bla",
|
||||||
|
["Unable to import test_domain: bla", "bla"],
|
||||||
|
False,
|
||||||
|
"component_import_err",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
config_util.ConfigExceptionInfo(
|
||||||
|
HomeAssistantError("bla"),
|
||||||
|
"config_validation_err",
|
||||||
|
"test_domain",
|
||||||
|
{"test_domain": []},
|
||||||
|
"https://example.com",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
"bla",
|
||||||
|
[
|
||||||
|
"Invalid config for 'test_domain': bla, "
|
||||||
|
"please check the docs at https://example.com",
|
||||||
|
"bla",
|
||||||
|
],
|
||||||
|
True,
|
||||||
|
"config_validation_err",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
config_util.ConfigExceptionInfo(
|
||||||
|
vol.Invalid("bla", ["path"]),
|
||||||
|
"config_validation_err",
|
||||||
|
"test_domain",
|
||||||
|
{"test_domain": []},
|
||||||
|
"https://example.com",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
"bla @ data['path']",
|
||||||
|
[
|
||||||
|
"Invalid config for 'test_domain': bla 'path', got None, "
|
||||||
|
"please check the docs at https://example.com",
|
||||||
|
"bla",
|
||||||
|
],
|
||||||
|
False,
|
||||||
|
"config_validation_err",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
config_util.ConfigExceptionInfo(
|
||||||
|
vol.Invalid("bla", ["path"]),
|
||||||
|
"platform_config_validation_err",
|
||||||
|
"test_domain",
|
||||||
|
{"test_domain": []},
|
||||||
|
"https://alt.example.com",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
"bla @ data['path']",
|
||||||
|
[
|
||||||
|
"Invalid config for 'test_domain': bla 'path', got None, "
|
||||||
|
"please check the docs at https://alt.example.com",
|
||||||
|
"bla",
|
||||||
|
],
|
||||||
|
False,
|
||||||
|
"platform_config_validation_err",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
config_util.ConfigExceptionInfo(
|
||||||
|
ImportError("bla"),
|
||||||
|
"platform_component_load_err",
|
||||||
|
"test_domain",
|
||||||
|
{"test_domain": []},
|
||||||
|
"https://example.com",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
"bla",
|
||||||
|
["Platform error: test_domain - bla", "bla"],
|
||||||
|
False,
|
||||||
|
"platform_component_load_err",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_component_config_error_processing(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
error: str,
|
||||||
|
exception_info_list: list[config_util.ConfigExceptionInfo],
|
||||||
|
messages: list[str],
|
||||||
|
show_stack_trace: bool,
|
||||||
|
translation_key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test component config error processing."""
|
||||||
|
test_integration = Mock(
|
||||||
|
domain="test_domain",
|
||||||
|
documentation="https://example.com",
|
||||||
|
get_platform=Mock(
|
||||||
|
return_value=Mock(
|
||||||
|
async_validate_config=AsyncMock(side_effect=ValueError("broken"))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.config.async_process_component_config",
|
||||||
|
return_value=config_util.IntegrationConfigInfo(None, exception_info_list),
|
||||||
|
), pytest.raises(ConfigValidationError) as ex:
|
||||||
|
await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass, {}, test_integration, raise_on_failure=True
|
||||||
|
)
|
||||||
|
records = [record for record in caplog.records if record.msg == messages[0]]
|
||||||
|
assert len(records) == 1
|
||||||
|
assert (records[0].exc_info is not None) == show_stack_trace
|
||||||
|
assert str(ex.value) == messages[0]
|
||||||
|
assert ex.value.translation_key == translation_key
|
||||||
|
assert ex.value.translation_domain == "homeassistant"
|
||||||
|
assert ex.value.translation_placeholders["domain"] == "test_domain"
|
||||||
|
assert all(message in caplog.text for message in messages)
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
|
with patch(
|
||||||
|
"homeassistant.config.async_process_component_config",
|
||||||
|
return_value=config_util.IntegrationConfigInfo(None, exception_info_list),
|
||||||
|
):
|
||||||
|
await config_util.async_process_component_and_handle_errors(
|
||||||
|
hass, {}, test_integration
|
||||||
|
)
|
||||||
|
assert all(message in caplog.text for message in messages)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -1713,7 +2045,7 @@ async def test_component_config_validation_error(
|
||||||
integration = await async_get_integration(
|
integration = await async_get_integration(
|
||||||
hass, domain_with_label.partition(" ")[0]
|
hass, domain_with_label.partition(" ")[0]
|
||||||
)
|
)
|
||||||
await config_util.async_process_component_config(
|
await config_util.async_process_component_and_handle_errors(
|
||||||
hass,
|
hass,
|
||||||
config,
|
config,
|
||||||
integration=integration,
|
integration=integration,
|
||||||
|
@ -1758,7 +2090,7 @@ async def test_component_config_validation_error_with_docs(
|
||||||
integration = await async_get_integration(
|
integration = await async_get_integration(
|
||||||
hass, domain_with_label.partition(" ")[0]
|
hass, domain_with_label.partition(" ")[0]
|
||||||
)
|
)
|
||||||
await config_util.async_process_component_config(
|
await config_util.async_process_component_and_handle_errors(
|
||||||
hass,
|
hass,
|
||||||
config,
|
config,
|
||||||
integration=integration,
|
integration=integration,
|
||||||
|
|
|
@ -374,7 +374,7 @@ async def test_platform_specific_config_validation(hass: HomeAssistant) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
with assert_setup_component(0, "switch"), patch(
|
with assert_setup_component(0, "switch"), patch(
|
||||||
"homeassistant.config.async_notify_setup_error"
|
"homeassistant.setup.async_notify_setup_error"
|
||||||
) as mock_notify:
|
) as mock_notify:
|
||||||
assert await setup.async_setup_component(
|
assert await setup.async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
|
@ -389,7 +389,7 @@ async def test_platform_specific_config_validation(hass: HomeAssistant) -> None:
|
||||||
hass.config.components.remove("switch")
|
hass.config.components.remove("switch")
|
||||||
|
|
||||||
with assert_setup_component(0), patch(
|
with assert_setup_component(0), patch(
|
||||||
"homeassistant.config.async_notify_setup_error"
|
"homeassistant.setup.async_notify_setup_error"
|
||||||
) as mock_notify:
|
) as mock_notify:
|
||||||
assert await setup.async_setup_component(
|
assert await setup.async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
|
@ -410,7 +410,7 @@ async def test_platform_specific_config_validation(hass: HomeAssistant) -> None:
|
||||||
hass.config.components.remove("switch")
|
hass.config.components.remove("switch")
|
||||||
|
|
||||||
with assert_setup_component(1, "switch"), patch(
|
with assert_setup_component(1, "switch"), patch(
|
||||||
"homeassistant.config.async_notify_setup_error"
|
"homeassistant.setup.async_notify_setup_error"
|
||||||
) as mock_notify:
|
) as mock_notify:
|
||||||
assert await setup.async_setup_component(
|
assert await setup.async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue