mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
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
@ -194,7 +194,9 @@ async def async_setup_platform(
|
||||
|
||||
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):
|
||||
return
|
||||
|
@ -138,6 +138,36 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"message": "Service {domain}.{service} not found."
|
||||
}
|
||||
|
@ -4,7 +4,10 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
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.core import HomeAssistant, ServiceCall, callback
|
||||
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)
|
||||
|
||||
config = await async_process_component_config(hass, conf, integration)
|
||||
config = await async_process_component_and_handle_errors(
|
||||
hass, conf, integration
|
||||
)
|
||||
|
||||
if config is None:
|
||||
raise HomeAssistantError("Config validation failed")
|
||||
|
@ -25,7 +25,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
ConfigValidationError,
|
||||
ServiceValidationError,
|
||||
TemplateError,
|
||||
Unauthorized,
|
||||
@ -417,14 +417,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def _reload_config(call: ServiceCall) -> None:
|
||||
"""Reload the platforms."""
|
||||
# Fetch updated manually configured items and validate
|
||||
if (
|
||||
config_yaml := await async_integration_yaml_config(hass, DOMAIN)
|
||||
) is None:
|
||||
# Raise in case we have an invalid configuration
|
||||
raise HomeAssistantError(
|
||||
"Error reloading manually configured MQTT items, "
|
||||
"check your configuration.yaml"
|
||||
try:
|
||||
config_yaml = await async_integration_yaml_config(
|
||||
hass, DOMAIN, raise_on_failure=True
|
||||
)
|
||||
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
|
||||
await async_check_config_schema(hass, config_yaml)
|
||||
|
||||
|
@ -34,8 +34,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
conf = await conf_util.async_process_component_config(
|
||||
hass, unprocessed_conf, await async_get_integration(hass, DOMAIN)
|
||||
integration = await async_get_integration(hass, DOMAIN)
|
||||
conf = await conf_util.async_process_component_and_handle_errors(
|
||||
hass, unprocessed_conf, integration
|
||||
)
|
||||
|
||||
if conf is None:
|
||||
|
@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable, Sequence
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from functools import reduce
|
||||
import logging
|
||||
import operator
|
||||
@ -12,7 +14,7 @@ from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
@ -54,7 +56,7 @@ from .const import (
|
||||
__version__,
|
||||
)
|
||||
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 .helpers import (
|
||||
config_per_platform,
|
||||
@ -66,13 +68,13 @@ from .helpers.entity_values import EntityValues
|
||||
from .helpers.typing import ConfigType
|
||||
from .loader import ComponentProtocol, Integration, IntegrationNotFound
|
||||
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.unit_system import get_unit_system, validate_unit_system
|
||||
from .util.yaml import SECRET_YAML, Secrets, load_yaml
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_PERSISTENT_ERRORS = "bootstrap_persistent_errors"
|
||||
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
|
||||
RE_ASCII = re.compile(r"\033\[[^m]*m")
|
||||
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(
|
||||
configs: Sequence[dict[str, Any]]
|
||||
) -> Sequence[dict[str, Any]]:
|
||||
@ -1025,21 +1067,193 @@ async def merge_packages_config(
|
||||
return config
|
||||
|
||||
|
||||
async def async_process_component_config( # noqa: C901
|
||||
hass: HomeAssistant, config: ConfigType, integration: Integration
|
||||
) -> ConfigType | None:
|
||||
"""Check component configuration and return processed configuration.
|
||||
@callback
|
||||
def _get_log_message_and_stack_print_pref(
|
||||
hass: HomeAssistant, domain: str, platform_exception: ConfigExceptionInfo
|
||||
) -> 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.
|
||||
"""
|
||||
domain = integration.domain
|
||||
integration_docs = integration.documentation
|
||||
config_exceptions: list[ConfigExceptionInfo] = []
|
||||
|
||||
try:
|
||||
component = integration.get_component()
|
||||
except LOAD_EXCEPTIONS as ex:
|
||||
_LOGGER.error("Unable to import %s: %s", domain, ex)
|
||||
return None
|
||||
except LOAD_EXCEPTIONS as exc:
|
||||
exc_info = ConfigExceptionInfo(
|
||||
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
|
||||
config_validator = None
|
||||
@ -1050,62 +1264,101 @@ async def async_process_component_config( # noqa: C901
|
||||
# If the config platform contains bad imports, make sure
|
||||
# that still fails.
|
||||
if err.name != f"{integration.pkg_path}.config":
|
||||
_LOGGER.error("Error importing config platform %s: %s", domain, err)
|
||||
return None
|
||||
exc_info = ConfigExceptionInfo(
|
||||
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(
|
||||
config_validator, "async_validate_config"
|
||||
):
|
||||
try:
|
||||
return ( # type: ignore[no-any-return]
|
||||
await config_validator.async_validate_config(hass, config)
|
||||
return IntegrationConfigInfo(
|
||||
await config_validator.async_validate_config(hass, config), []
|
||||
)
|
||||
except (vol.Invalid, HomeAssistantError) as ex:
|
||||
async_log_config_validator_error(
|
||||
ex, domain, config, hass, integration.documentation
|
||||
except (vol.Invalid, HomeAssistantError) as exc:
|
||||
exc_info = ConfigExceptionInfo(
|
||||
exc,
|
||||
ConfigErrorTranslationKey.CONFIG_VALIDATION_ERR,
|
||||
domain,
|
||||
config,
|
||||
integration_docs,
|
||||
)
|
||||
return None
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unknown error calling %s config validator", domain)
|
||||
return None
|
||||
config_exceptions.append(exc_info)
|
||||
return IntegrationConfigInfo(None, config_exceptions)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
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
|
||||
if hasattr(component, "CONFIG_SCHEMA"):
|
||||
try:
|
||||
return component.CONFIG_SCHEMA(config) # type: ignore[no-any-return]
|
||||
except vol.Invalid as ex:
|
||||
async_log_schema_error(ex, domain, config, hass, integration.documentation)
|
||||
return None
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unknown error calling %s CONFIG_SCHEMA", domain)
|
||||
return None
|
||||
return IntegrationConfigInfo(component.CONFIG_SCHEMA(config), [])
|
||||
except vol.Invalid as exc:
|
||||
exc_info = ConfigExceptionInfo(
|
||||
exc,
|
||||
ConfigErrorTranslationKey.CONFIG_VALIDATION_ERR,
|
||||
domain,
|
||||
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_BASE", getattr(component, "PLATFORM_SCHEMA", 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):
|
||||
# Validate component specific platform schema
|
||||
platform_name = f"{domain}.{p_name}"
|
||||
try:
|
||||
p_validated = component_platform_schema(p_config)
|
||||
except vol.Invalid as ex:
|
||||
async_log_schema_error(
|
||||
ex, domain, p_config, hass, integration.documentation
|
||||
)
|
||||
continue
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception(
|
||||
(
|
||||
"Unknown error validating %s platform config with %s component"
|
||||
" platform schema"
|
||||
),
|
||||
p_name,
|
||||
except vol.Invalid as exc:
|
||||
exc_info = ConfigExceptionInfo(
|
||||
exc,
|
||||
ConfigErrorTranslationKey.PLATFORM_CONFIG_VALIDATION_ERR,
|
||||
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
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
@ -1117,38 +1370,53 @@ async def async_process_component_config( # noqa: C901
|
||||
|
||||
try:
|
||||
p_integration = await async_get_integration_with_requirements(hass, p_name)
|
||||
except (RequirementsNotFound, IntegrationNotFound) as ex:
|
||||
_LOGGER.error("Platform error: %s - %s", domain, ex)
|
||||
except (RequirementsNotFound, IntegrationNotFound) as exc:
|
||||
exc_info = ConfigExceptionInfo(
|
||||
exc,
|
||||
ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_ERR,
|
||||
platform_name,
|
||||
p_config,
|
||||
integration_docs,
|
||||
)
|
||||
config_exceptions.append(exc_info)
|
||||
continue
|
||||
|
||||
try:
|
||||
platform = p_integration.get_platform(domain)
|
||||
except LOAD_EXCEPTIONS:
|
||||
_LOGGER.exception("Platform error: %s", domain)
|
||||
except LOAD_EXCEPTIONS as exc:
|
||||
exc_info = ConfigExceptionInfo(
|
||||
exc,
|
||||
ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC,
|
||||
platform_name,
|
||||
p_config,
|
||||
integration_docs,
|
||||
)
|
||||
config_exceptions.append(exc_info)
|
||||
continue
|
||||
|
||||
# Validate platform specific schema
|
||||
if hasattr(platform, "PLATFORM_SCHEMA"):
|
||||
try:
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_config)
|
||||
except vol.Invalid as ex:
|
||||
async_log_schema_error(
|
||||
ex,
|
||||
f"{domain}.{p_name}",
|
||||
except vol.Invalid as exc:
|
||||
exc_info = ConfigExceptionInfo(
|
||||
exc,
|
||||
ConfigErrorTranslationKey.PLATFORM_CONFIG_VALIDATION_ERR,
|
||||
platform_name,
|
||||
p_config,
|
||||
hass,
|
||||
p_integration.documentation,
|
||||
)
|
||||
config_exceptions.append(exc_info)
|
||||
continue
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception(
|
||||
(
|
||||
"Unknown error validating config for %s platform for %s"
|
||||
" component with PLATFORM_SCHEMA"
|
||||
),
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
exc_info = ConfigExceptionInfo(
|
||||
exc,
|
||||
ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR,
|
||||
p_name,
|
||||
domain,
|
||||
p_config,
|
||||
p_integration.documentation,
|
||||
)
|
||||
config_exceptions.append(exc_info)
|
||||
continue
|
||||
|
||||
platforms.append(p_validated)
|
||||
@ -1158,7 +1426,7 @@ async def async_process_component_config( # noqa: C901
|
||||
config = config_without_domain(config, domain)
|
||||
config[domain] = platforms
|
||||
|
||||
return config
|
||||
return IntegrationConfigInfo(config, config_exceptions)
|
||||
|
||||
|
||||
@callback
|
||||
@ -1183,36 +1451,6 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> str | None:
|
||||
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:
|
||||
"""Return if safe mode is enabled.
|
||||
|
||||
|
@ -26,6 +26,31 @@ class HomeAssistantError(Exception):
|
||||
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):
|
||||
"""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)
|
||||
|
||||
processed_conf = await conf_util.async_process_component_config(
|
||||
processed_conf = await conf_util.async_process_component_and_handle_errors(
|
||||
self.hass, conf, integration
|
||||
)
|
||||
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import Iterable
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Literal, overload
|
||||
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
@ -60,7 +60,7 @@ async def _resetup_platform(
|
||||
"""Resetup a platform."""
|
||||
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
|
||||
)
|
||||
|
||||
@ -136,14 +136,41 @@ async def _async_reconfig_platform(
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
@overload
|
||||
async def async_integration_yaml_config(
|
||||
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:
|
||||
"""Fetch the latest yaml configuration for an integration."""
|
||||
integration = await async_get_integration(hass, integration_name)
|
||||
|
||||
return await conf_util.async_process_component_config(
|
||||
hass, await conf_util.async_hass_config_yaml(hass), integration
|
||||
config = await conf_util.async_hass_config_yaml(hass)
|
||||
return await conf_util.async_process_component_and_handle_errors(
|
||||
hass, config, integration, raise_on_failure=raise_on_failure
|
||||
)
|
||||
|
||||
|
||||
|
@ -11,14 +11,13 @@ from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
from . import config as conf_util, core, loader, requirements
|
||||
from .config import async_notify_setup_error
|
||||
from .const import (
|
||||
EVENT_COMPONENT_LOADED,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
PLATFORM_FORMAT,
|
||||
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 .helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from .helpers.typing import ConfigType
|
||||
@ -56,10 +55,47 @@ DATA_SETUP_TIME = "setup_time"
|
||||
|
||||
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_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
|
||||
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.
|
||||
@ -217,10 +253,18 @@ async def _async_setup_component(
|
||||
log_error(f"Unable to import component: {err}", err)
|
||||
return False
|
||||
|
||||
processed_config = await conf_util.async_process_component_config(
|
||||
integration_config_info = await conf_util.async_process_component_config(
|
||||
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:
|
||||
log_error("Invalid config.")
|
||||
return False
|
||||
|
@ -984,7 +984,10 @@ def assert_setup_component(count, domain=None):
|
||||
async def mock_psc(hass, config_input, integration):
|
||||
"""Mock the prepare_setup_component to capture config."""
|
||||
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)
|
||||
_LOGGER.debug(
|
||||
"Configuration for %s, Validated: %s, Original %s",
|
||||
@ -992,7 +995,7 @@ def assert_setup_component(count, domain=None):
|
||||
config[domain_input],
|
||||
config_input.get(domain_input),
|
||||
)
|
||||
return res
|
||||
return integration_config_info
|
||||
|
||||
assert isinstance(config, dict)
|
||||
with patch("homeassistant.config.async_process_component_config", mock_psc):
|
||||
|
@ -3,10 +3,12 @@ import logging
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigValidationError, HomeAssistantError
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity_platform import async_get_platforms
|
||||
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")
|
||||
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(
|
||||
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")
|
||||
with patch.object(config, "YAML_CONFIG_FILE", yaml_path):
|
||||
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:
|
||||
|
@ -1013,7 +1013,10 @@ async def test_bootstrap_dependencies(
|
||||
with patch(
|
||||
"homeassistant.setup.loader.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})
|
||||
await bootstrap.async_setup_multi_components(hass, {integration}, {})
|
||||
await hass.async_block_till_done()
|
||||
|
@ -30,6 +30,7 @@ from homeassistant.const import (
|
||||
__version__,
|
||||
)
|
||||
from homeassistant.core import ConfigSource, HomeAssistant, HomeAssistantError
|
||||
from homeassistant.exceptions import ConfigValidationError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
import homeassistant.helpers.check_config as check_config
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@ -1427,71 +1428,132 @@ async def test_component_config_exceptions(
|
||||
) -> None:
|
||||
"""Test unexpected exceptions validating component config."""
|
||||
# Config validator
|
||||
test_integration = Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(
|
||||
return_value=Mock(
|
||||
async_validate_config=AsyncMock(side_effect=ValueError("broken"))
|
||||
)
|
||||
),
|
||||
)
|
||||
assert (
|
||||
await config_util.async_process_component_config(
|
||||
hass,
|
||||
{},
|
||||
integration=Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(
|
||||
return_value=Mock(
|
||||
async_validate_config=AsyncMock(
|
||||
side_effect=ValueError("broken")
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass, {}, integration=test_integration
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert "ValueError: broken" 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()
|
||||
assert (
|
||||
await config_util.async_process_component_config(
|
||||
hass,
|
||||
{},
|
||||
integration=Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(return_value=None),
|
||||
get_component=Mock(
|
||||
return_value=Mock(
|
||||
CONFIG_SCHEMA=Mock(side_effect=ValueError("broken"))
|
||||
)
|
||||
),
|
||||
),
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass, {}, integration=test_integration, raise_on_failure=False
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert "Invalid config for 'test_domain': broken" 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 "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
|
||||
)
|
||||
assert "ValueError: broken" 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
|
||||
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,
|
||||
{"test_domain": {"platform": "test_platform"}},
|
||||
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")),
|
||||
)
|
||||
),
|
||||
),
|
||||
integration=test_integration,
|
||||
raise_on_failure=False,
|
||||
) == {"test_domain": []}
|
||||
assert "ValueError: broken" in caplog.text
|
||||
assert (
|
||||
"Unknown error validating test_platform platform config "
|
||||
"with test_domain component platform schema"
|
||||
"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:
|
||||
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
|
||||
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(
|
||||
"homeassistant.config.async_get_integration_with_requirements",
|
||||
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,
|
||||
{"test_domain": {"platform": "test_platform"}},
|
||||
integration=Mock(
|
||||
domain="test_domain",
|
||||
get_platform=Mock(return_value=None),
|
||||
get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])),
|
||||
),
|
||||
integration=test_integration,
|
||||
raise_on_failure=False,
|
||||
) == {"test_domain": []}
|
||||
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 (
|
||||
"Unknown error validating config for test_platform platform for test_domain"
|
||||
" 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
|
||||
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 (
|
||||
await config_util.async_process_component_config(
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{"test_domain": {}},
|
||||
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",
|
||||
)
|
||||
),
|
||||
),
|
||||
integration=test_integration,
|
||||
raise_on_failure=False,
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
"Error importing config platform test_domain: ModuleNotFoundError: No module"
|
||||
" named 'not_installed_something'" in caplog.text
|
||||
"Error importing config platform test_domain: ModuleNotFoundError: "
|
||||
"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
|
||||
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 (
|
||||
await config_util.async_process_component_config(
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
{"test_domain": {}},
|
||||
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'"
|
||||
)
|
||||
),
|
||||
),
|
||||
integration=test_integration,
|
||||
raise_on_failure=False,
|
||||
)
|
||||
is None
|
||||
)
|
||||
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(
|
||||
@ -1713,7 +2045,7 @@ async def test_component_config_validation_error(
|
||||
integration = await async_get_integration(
|
||||
hass, domain_with_label.partition(" ")[0]
|
||||
)
|
||||
await config_util.async_process_component_config(
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
config,
|
||||
integration=integration,
|
||||
@ -1758,7 +2090,7 @@ async def test_component_config_validation_error_with_docs(
|
||||
integration = await async_get_integration(
|
||||
hass, domain_with_label.partition(" ")[0]
|
||||
)
|
||||
await config_util.async_process_component_config(
|
||||
await config_util.async_process_component_and_handle_errors(
|
||||
hass,
|
||||
config,
|
||||
integration=integration,
|
||||
|
@ -374,7 +374,7 @@ async def test_platform_specific_config_validation(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
with assert_setup_component(0, "switch"), patch(
|
||||
"homeassistant.config.async_notify_setup_error"
|
||||
"homeassistant.setup.async_notify_setup_error"
|
||||
) as mock_notify:
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
@ -389,7 +389,7 @@ async def test_platform_specific_config_validation(hass: HomeAssistant) -> None:
|
||||
hass.config.components.remove("switch")
|
||||
|
||||
with assert_setup_component(0), patch(
|
||||
"homeassistant.config.async_notify_setup_error"
|
||||
"homeassistant.setup.async_notify_setup_error"
|
||||
) as mock_notify:
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
@ -410,7 +410,7 @@ async def test_platform_specific_config_validation(hass: HomeAssistant) -> None:
|
||||
hass.config.components.remove("switch")
|
||||
|
||||
with assert_setup_component(1, "switch"), patch(
|
||||
"homeassistant.config.async_notify_setup_error"
|
||||
"homeassistant.setup.async_notify_setup_error"
|
||||
) as mock_notify:
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
|
Loading…
x
Reference in New Issue
Block a user