mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Fix line length violations in script/ (#170759)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
018e3a4765
commit
f0396aca8a
@@ -14,9 +14,10 @@ from typing import Any
|
||||
from homeassistant.util.yaml.loader import load_yaml
|
||||
from script.hassfest.model import Config, Integration
|
||||
|
||||
# Requirements which can't be installed on all systems because they rely on additional
|
||||
# system packages. Requirements listed in EXCLUDED_REQUIREMENTS_ALL will be commented-out
|
||||
# in requirements_all.txt and requirements_test_all.txt.
|
||||
# Requirements which can't be installed on all systems because they
|
||||
# rely on additional system packages. Requirements listed in
|
||||
# EXCLUDED_REQUIREMENTS_ALL will be commented-out in
|
||||
# requirements_all.txt and requirements_test_all.txt.
|
||||
EXCLUDED_REQUIREMENTS_ALL = {
|
||||
"atenpdu", # depends on pysnmp which is not maintained at this time
|
||||
"avion",
|
||||
@@ -90,9 +91,11 @@ enum34==1000000000.0.0
|
||||
typing==1000000000.0.0
|
||||
uuid==1000000000.0.0
|
||||
|
||||
# httpx requires httpcore, and httpcore requires anyio and h11, but the version constraints on
|
||||
# these requirements are quite loose. As the entire stack has some outstanding issues, and
|
||||
# even newer versions seem to introduce new issues, it's useful for us to pin all these
|
||||
# httpx requires httpcore, and httpcore requires anyio and h11,
|
||||
# but the version constraints on these requirements are quite
|
||||
# loose. As the entire stack has some outstanding issues, and
|
||||
# even newer versions seem to introduce new issues, it's useful
|
||||
# for us to pin all these
|
||||
# requirements so we can directly link HA versions to these library versions.
|
||||
anyio==4.10.0
|
||||
h11==0.16.0
|
||||
|
||||
@@ -123,7 +123,10 @@ def get_config() -> Config:
|
||||
"--skip-plugins",
|
||||
type=validate_plugins,
|
||||
default=[],
|
||||
help=f"Comma-separated list of plugins to skip. Valid plugin names: {ALL_PLUGIN_NAMES}",
|
||||
help=(
|
||||
"Comma-separated list of plugins to skip."
|
||||
f" Valid plugin names: {ALL_PLUGIN_NAMES}"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--core-path",
|
||||
|
||||
@@ -35,7 +35,8 @@ def validate(integrations: dict[str, Integration], config: Config) -> None:
|
||||
if application_credentials_path.read_text(encoding="utf-8") != content:
|
||||
config.add_error(
|
||||
"application_credentials",
|
||||
"File application_credentials.py is not up to date. Run python3 -m script.hassfest",
|
||||
"File application_credentials.py is not up to date."
|
||||
" Run python3 -m script.hassfest",
|
||||
fixable=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -54,7 +54,8 @@ def _validate_brand(
|
||||
if sub_integration not in integrations:
|
||||
config.add_error(
|
||||
"brand",
|
||||
f"{brand.path.name}: References unknown integration {sub_integration}",
|
||||
f"{brand.path.name}: References unknown"
|
||||
f" integration {sub_integration}",
|
||||
)
|
||||
|
||||
if brand.domain in integrations and (
|
||||
|
||||
@@ -47,15 +47,21 @@ def validate_field_schema(condition_schema: dict[str, Any]) -> dict[str, Any]:
|
||||
# Check if context key is allowed for this selector type
|
||||
allowed_keys = selector_class.allowed_context_keys
|
||||
if context_key not in allowed_keys:
|
||||
allowed = (
|
||||
", ".join(sorted(allowed_keys)) if allowed_keys else "none"
|
||||
)
|
||||
raise vol.Invalid(
|
||||
f"Invalid context key '{context_key}' for selector type '{selector_class.selector_type}'. "
|
||||
f"Allowed keys: {', '.join(sorted(allowed_keys)) if allowed_keys else 'none'}"
|
||||
f"Invalid context key '{context_key}'"
|
||||
f" for selector type"
|
||||
f" '{selector_class.selector_type}'."
|
||||
f" Allowed keys: {allowed}"
|
||||
)
|
||||
|
||||
# Check if the referenced field exists in condition schema or target
|
||||
if not isinstance(field_ref, str):
|
||||
raise vol.Invalid(
|
||||
f"Context value for '{context_key}' must be a string field reference"
|
||||
f"Context value for '{context_key}'"
|
||||
" must be a string field reference"
|
||||
)
|
||||
|
||||
# Check if field exists in condition schema fields or target
|
||||
@@ -68,9 +74,15 @@ def validate_field_schema(condition_schema: dict[str, Any]) -> dict[str, Any]:
|
||||
if field_selector_class.selector_type not in allowed_keys.get(
|
||||
context_key, set()
|
||||
):
|
||||
allowed_types = ", ".join(allowed_keys.get(context_key, set()))
|
||||
sel_type = field_selector_class.selector_type
|
||||
raise vol.Invalid(
|
||||
f"The context '{context_key}' for '{field_name}' references '{field_ref}', but '{context_key}' "
|
||||
f"does not allow selectors of type '{field_selector_class.selector_type}'. Allowed selector types: {', '.join(allowed_keys.get(context_key, set()))}"
|
||||
f"The context '{context_key}' for"
|
||||
f" '{field_name}' references"
|
||||
f" '{field_ref}', but"
|
||||
f" '{context_key}' does not allow"
|
||||
f" selectors of type '{sel_type}'."
|
||||
f" Allowed types: {allowed_types}"
|
||||
)
|
||||
if not field_exists and "target" in condition_schema:
|
||||
# Target is a special field that always exists when defined
|
||||
@@ -78,15 +90,21 @@ def validate_field_schema(condition_schema: dict[str, Any]) -> dict[str, Any]:
|
||||
if field_exists and "target" not in allowed_keys.get(
|
||||
context_key, set()
|
||||
):
|
||||
allowed_types = ", ".join(allowed_keys.get(context_key, set()))
|
||||
raise vol.Invalid(
|
||||
f"The context '{context_key}' for '{field_name}' references 'target', but '{context_key}' "
|
||||
f"does not allow 'target'. Allowed selector types: {', '.join(allowed_keys.get(context_key, set()))}"
|
||||
f"The context '{context_key}' for"
|
||||
f" '{field_name}' references"
|
||||
f" 'target', but '{context_key}'"
|
||||
" does not allow 'target'."
|
||||
f" Allowed types: {allowed_types}"
|
||||
)
|
||||
|
||||
if not field_exists:
|
||||
raise vol.Invalid(
|
||||
f"Context reference '{field_ref}' for key '{context_key}' does not exist "
|
||||
f"in condition schema fields or target"
|
||||
f"Context reference '{field_ref}'"
|
||||
f" for key '{context_key}' does"
|
||||
" not exist in condition schema"
|
||||
" fields or target"
|
||||
)
|
||||
|
||||
return condition_schema
|
||||
@@ -98,9 +116,9 @@ FIELD_SCHEMA = vol.Schema(
|
||||
vol.Optional("default"): exists,
|
||||
vol.Optional("required"): bool,
|
||||
vol.Optional(CONF_SELECTOR): selector.validate_selector,
|
||||
vol.Optional("context"): {
|
||||
str: str # key is context key, value is field name in the schema which value should be used
|
||||
}, # Will be validated in validate_field_schema
|
||||
# key is context key, value is field name in schema
|
||||
# Validated in validate_field_schema
|
||||
vol.Optional("context"): {str: str},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -241,10 +259,9 @@ def validate_conditions(config: Config, integration: Integration) -> None: # no
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"conditions",
|
||||
(
|
||||
f"Condition {condition_name} has a field {field_name} with no "
|
||||
f"name {error_msg_suffix}"
|
||||
),
|
||||
f"Condition {condition_name} has a"
|
||||
f" field {field_name} with no"
|
||||
f" name {error_msg_suffix}",
|
||||
)
|
||||
|
||||
if "selector" in field_schema:
|
||||
@@ -257,7 +274,14 @@ def validate_conditions(config: Config, integration: Integration) -> None: # no
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"conditions",
|
||||
f"Condition {condition_name} has a field {field_name} with a selector with a translation key {translation_key} that is not in the translations file",
|
||||
f"Condition {condition_name}"
|
||||
f" has a field"
|
||||
f" {field_name} with a"
|
||||
" selector with a"
|
||||
" translation key"
|
||||
f" {translation_key}"
|
||||
" that is not in the"
|
||||
" translations file",
|
||||
)
|
||||
|
||||
# The same check is done for the description in each of the sections of the
|
||||
@@ -274,7 +298,10 @@ def validate_conditions(config: Config, integration: Integration) -> None: # no
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"conditions",
|
||||
f"Condition {condition_name} has a section {section_name} with no name {error_msg_suffix}",
|
||||
f"Condition {condition_name}"
|
||||
f" has a section"
|
||||
f" {section_name} with no"
|
||||
f" name {error_msg_suffix}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -128,9 +128,10 @@ def _generate_integrations(
|
||||
"translated_name": set(),
|
||||
}
|
||||
|
||||
# Not all integrations will have an item in the brands collection.
|
||||
# The config flow data index will be the union of the integrations without a brands item
|
||||
# and the brand domain names from the brands collection.
|
||||
# Not all integrations will have an item in the brands
|
||||
# collection. The config flow data index will be the union of
|
||||
# the integrations without a brands item and the brand domain
|
||||
# names from the brands collection.
|
||||
|
||||
# Compile a set of integrations which are referenced from at least one brand's
|
||||
# integrations list. These integrations will not be present in the root level of the
|
||||
|
||||
@@ -70,7 +70,8 @@ class ImportCollector(ast.NodeVisitor):
|
||||
return
|
||||
|
||||
if node.module.startswith("homeassistant.components."):
|
||||
# from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME
|
||||
# from homeassistant.components.alexa.smart_home
|
||||
# import EVENT_ALEXA_SMART_HOME
|
||||
# from homeassistant.components.logbook import bla
|
||||
self._add_reference(node.module.split(".")[2])
|
||||
|
||||
@@ -279,7 +280,9 @@ def _check_circular_deps(
|
||||
if domain == start_domain:
|
||||
integrations[start_domain].add_error(
|
||||
"dependencies",
|
||||
f"Found a circular dependency with {integration.domain} ({', '.join(checking)})",
|
||||
f"Found a circular dependency with"
|
||||
f" {integration.domain}"
|
||||
f" ({', '.join(checking)})",
|
||||
)
|
||||
break
|
||||
|
||||
@@ -291,7 +294,10 @@ def _check_circular_deps(
|
||||
if domain == start_domain:
|
||||
integrations[start_domain].add_error(
|
||||
"dependencies",
|
||||
f"Found a circular dependency with after dependencies of {integration.domain} ({', '.join(checking)})",
|
||||
f"Found a circular dependency"
|
||||
" with after dependencies of"
|
||||
f" {integration.domain}"
|
||||
f" ({', '.join(checking)})",
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ def icon_value_validator(value: Any) -> str:
|
||||
value = cv.string_with_no_html(value)
|
||||
if not value.startswith("mdi:"):
|
||||
raise vol.Invalid(
|
||||
"The icon needs to be a valid icon from Material Design Icons and start with `mdi:`"
|
||||
"The icon needs to be a valid icon from Material"
|
||||
" Design Icons and start with `mdi:`"
|
||||
)
|
||||
return str(value)
|
||||
|
||||
|
||||
@@ -82,7 +82,9 @@ def validate(integrations: dict[str, Integration], config: Config) -> None:
|
||||
if integration.domain in MISSING_INTEGRATION_TYPE:
|
||||
integration.add_error(
|
||||
"integration_type",
|
||||
"Integration has an `integration_type` in the manifest but is still listed in MISSING_INTEGRATION_TYPE",
|
||||
"Integration has an `integration_type`"
|
||||
" in the manifest but is still listed"
|
||||
" in MISSING_INTEGRATION_TYPE",
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -91,5 +93,6 @@ def validate(integrations: dict[str, Integration], config: Config) -> None:
|
||||
|
||||
integration.add_error(
|
||||
"integration_type",
|
||||
"Integration has a config flow but is missing an `integration_type` in the manifest",
|
||||
"Integration has a config flow but is missing"
|
||||
" an `integration_type` in the manifest",
|
||||
)
|
||||
|
||||
@@ -18,7 +18,8 @@ def generate_and_validate(integrations: dict[str, Integration]) -> str:
|
||||
if not isinstance(preview_features, dict):
|
||||
integration.add_error(
|
||||
"labs",
|
||||
f"preview_features must be a dict, got {type(preview_features).__name__}",
|
||||
"preview_features must be a dict,"
|
||||
f" got {type(preview_features).__name__}",
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -28,13 +29,17 @@ def generate_and_validate(integrations: dict[str, Integration]) -> str:
|
||||
if not isinstance(preview_feature_id, str):
|
||||
integration.add_error(
|
||||
"labs",
|
||||
f"preview_features keys must be strings, got {type(preview_feature_id).__name__}",
|
||||
"preview_features keys must be"
|
||||
" strings, got"
|
||||
f" {type(preview_feature_id).__name__}",
|
||||
)
|
||||
break
|
||||
if not isinstance(preview_feature_config, dict):
|
||||
integration.add_error(
|
||||
"labs",
|
||||
f"preview_features[{preview_feature_id}] must be a dict, got {type(preview_feature_config).__name__}",
|
||||
f"preview_features[{preview_feature_id}]"
|
||||
" must be a dict, got"
|
||||
f" {type(preview_feature_config).__name__}",
|
||||
)
|
||||
break
|
||||
# Include the full feature configuration
|
||||
|
||||
@@ -25,7 +25,8 @@ def validate(integrations: dict[str, Integration], config: Config) -> None:
|
||||
if data["project"]["requires-python"] != required_py_version:
|
||||
config.add_error(
|
||||
"metadata",
|
||||
f"'project.requires-python' value doesn't match '{required_py_version}'",
|
||||
"'project.requires-python' value doesn't"
|
||||
f" match '{required_py_version}'",
|
||||
)
|
||||
except KeyError:
|
||||
config.add_error("metadata", "No 'options.python_requires' key found!")
|
||||
|
||||
@@ -2184,16 +2184,23 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
|
||||
integration.add_error(
|
||||
"quality_scale",
|
||||
(
|
||||
"New integrations marked as internal should be added to NO_QUALITY_SCALE in script/hassfest/quality_scale.py."
|
||||
"New integrations marked as internal"
|
||||
" should be added to NO_QUALITY_SCALE"
|
||||
" in script/hassfest/quality_scale.py."
|
||||
if integration.quality_scale == "internal"
|
||||
else "Quality scale definition not found. New integrations are required to at least reach the Bronze tier."
|
||||
else "Quality scale definition not found."
|
||||
" New integrations are required to at"
|
||||
" least reach the Bronze tier."
|
||||
),
|
||||
)
|
||||
return
|
||||
if declared_quality_scale is not None:
|
||||
integration.add_error(
|
||||
"quality_scale",
|
||||
"Quality scale definition not found. Integrations that set a manifest quality scale must have a quality scale definition.",
|
||||
"Quality scale definition not found."
|
||||
" Integrations that set a manifest quality"
|
||||
" scale must have a quality scale"
|
||||
" definition.",
|
||||
)
|
||||
return
|
||||
return
|
||||
@@ -2212,7 +2219,9 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
|
||||
if integration.domain in INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE:
|
||||
integration.add_error(
|
||||
"quality_scale",
|
||||
"Quality scale file found! Please remove from `INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE`"
|
||||
"Quality scale file found! Please"
|
||||
" remove from"
|
||||
" `INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE`"
|
||||
" in script/hassfest/quality_scale.py",
|
||||
)
|
||||
return
|
||||
@@ -2222,7 +2231,8 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
|
||||
):
|
||||
integration.add_error(
|
||||
"quality_scale",
|
||||
"This integration is graded and should be removed from `INTEGRATIONS_WITHOUT_SCALE`"
|
||||
"This integration is graded and should be"
|
||||
" removed from `INTEGRATIONS_WITHOUT_SCALE`"
|
||||
" in script/hassfest/quality_scale.py",
|
||||
)
|
||||
return
|
||||
@@ -2233,7 +2243,10 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
|
||||
integration.add_error(
|
||||
"quality_scale",
|
||||
(
|
||||
"New integrations marked as internal should be added to INTEGRATIONS_WITHOUT_SCALE in script/hassfest/quality_scale.py."
|
||||
"New integrations marked as internal"
|
||||
" should be added to"
|
||||
" INTEGRATIONS_WITHOUT_SCALE in"
|
||||
" script/hassfest/quality_scale.py."
|
||||
if integration.quality_scale == "internal"
|
||||
else "New integrations are required to at least reach the Bronze tier."
|
||||
),
|
||||
@@ -2287,7 +2300,9 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
|
||||
)
|
||||
integration.add_error(
|
||||
"quality_scale",
|
||||
f"Quality scale tier {scale.name.lower()} requires quality scale rules to be met:\n{friendly_rule_str}",
|
||||
f"Quality scale tier {scale.name.lower()}"
|
||||
" requires quality scale rules to be"
|
||||
f" met:\n{friendly_rule_str}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -660,8 +660,10 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]:
|
||||
):
|
||||
integration.add_error(
|
||||
"requirements",
|
||||
f"Integration {integration.domain} runtime files dependency exceptions "
|
||||
"have been resolved, please remove from `FORBIDDEN_PACKAGE_FILES_EXCEPTIONS`",
|
||||
f"Integration {integration.domain} runtime"
|
||||
" files dependency exceptions have been"
|
||||
" resolved, please remove from"
|
||||
" `FORBIDDEN_PACKAGE_FILES_EXCEPTIONS`",
|
||||
)
|
||||
|
||||
return all_requirements
|
||||
@@ -767,7 +769,8 @@ def check_dependency_files(
|
||||
integration.add_warning_or_error(
|
||||
pkg in package_exceptions,
|
||||
"requirements",
|
||||
f"Package {pkg} has a forbidden top level directory '{dir_name}' in {package}",
|
||||
f"Package {pkg} has a forbidden top level"
|
||||
f" directory '{dir_name}' in {package}",
|
||||
)
|
||||
for file_name in results["file_names"]:
|
||||
integration.add_warning_or_error(
|
||||
|
||||
@@ -189,7 +189,9 @@ def check_extraneous_translation_fields(
|
||||
for field in translation_fields - section_fields:
|
||||
integration.add_error(
|
||||
"services",
|
||||
f"Service {service_name} has a field {field} in the translations file that is not in the schema",
|
||||
f"Service {service_name} has a field"
|
||||
f" {field} in the translations file"
|
||||
" that is not in the schema",
|
||||
)
|
||||
|
||||
|
||||
@@ -313,7 +315,9 @@ def validate_services(config: Config, integration: Integration) -> None: # noqa
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"services",
|
||||
f"Service {service_name} has a field {field_name} with no name {error_msg_suffix}",
|
||||
f"Service {service_name} has a"
|
||||
f" field {field_name} with"
|
||||
f" no name {error_msg_suffix}",
|
||||
)
|
||||
|
||||
if "selector" in field_schema:
|
||||
@@ -326,7 +330,14 @@ def validate_services(config: Config, integration: Integration) -> None: # noqa
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"services",
|
||||
f"Service {service_name} has a field {field_name} with a selector with a translation key {translation_key} that is not in the translations file",
|
||||
f"Service {service_name}"
|
||||
f" has a field"
|
||||
f" {field_name} with a"
|
||||
" selector with a"
|
||||
" translation key"
|
||||
f" {translation_key}"
|
||||
" that is not in the"
|
||||
" translations file",
|
||||
)
|
||||
|
||||
# The same check is done for the description in each of the sections of the
|
||||
@@ -341,7 +352,10 @@ def validate_services(config: Config, integration: Integration) -> None: # noqa
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"services",
|
||||
f"Service {service_name} has a section {section_name} with no name {error_msg_suffix}",
|
||||
f"Service {service_name}"
|
||||
f" has a section"
|
||||
f" {section_name} with no"
|
||||
f" name {error_msg_suffix}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
+42
-14
@@ -47,15 +47,21 @@ def validate_field_schema(trigger_schema: dict[str, Any]) -> dict[str, Any]:
|
||||
# Check if context key is allowed for this selector type
|
||||
allowed_keys = selector_class.allowed_context_keys
|
||||
if context_key not in allowed_keys:
|
||||
allowed = (
|
||||
", ".join(sorted(allowed_keys)) if allowed_keys else "none"
|
||||
)
|
||||
raise vol.Invalid(
|
||||
f"Invalid context key '{context_key}' for selector type '{selector_class.selector_type}'. "
|
||||
f"Allowed keys: {', '.join(sorted(allowed_keys)) if allowed_keys else 'none'}"
|
||||
f"Invalid context key '{context_key}'"
|
||||
f" for selector type"
|
||||
f" '{selector_class.selector_type}'."
|
||||
f" Allowed keys: {allowed}"
|
||||
)
|
||||
|
||||
# Check if the referenced field exists in trigger schema or target
|
||||
if not isinstance(field_ref, str):
|
||||
raise vol.Invalid(
|
||||
f"Context value for '{context_key}' must be a string field reference"
|
||||
f"Context value for '{context_key}'"
|
||||
" must be a string field reference"
|
||||
)
|
||||
|
||||
# Check if field exists in trigger schema fields or target
|
||||
@@ -68,9 +74,15 @@ def validate_field_schema(trigger_schema: dict[str, Any]) -> dict[str, Any]:
|
||||
if field_selector_class.selector_type not in allowed_keys.get(
|
||||
context_key, set()
|
||||
):
|
||||
allowed_types = ", ".join(allowed_keys.get(context_key, set()))
|
||||
sel_type = field_selector_class.selector_type
|
||||
raise vol.Invalid(
|
||||
f"The context '{context_key}' for '{field_name}' references '{field_ref}', but '{context_key}' "
|
||||
f"does not allow selectors of type '{field_selector_class.selector_type}'. Allowed selector types: {', '.join(allowed_keys.get(context_key, set()))}"
|
||||
f"The context '{context_key}' for"
|
||||
f" '{field_name}' references"
|
||||
f" '{field_ref}', but"
|
||||
f" '{context_key}' does not allow"
|
||||
f" selectors of type '{sel_type}'."
|
||||
f" Allowed types: {allowed_types}"
|
||||
)
|
||||
if not field_exists and "target" in trigger_schema:
|
||||
# Target is a special field that always exists when defined
|
||||
@@ -78,15 +90,21 @@ def validate_field_schema(trigger_schema: dict[str, Any]) -> dict[str, Any]:
|
||||
if field_exists and "target" not in allowed_keys.get(
|
||||
context_key, set()
|
||||
):
|
||||
allowed_types = ", ".join(allowed_keys.get(context_key, set()))
|
||||
raise vol.Invalid(
|
||||
f"The context '{context_key}' for '{field_name}' references 'target', but '{context_key}' "
|
||||
f"does not allow 'target'. Allowed selector types: {', '.join(allowed_keys.get(context_key, set()))}"
|
||||
f"The context '{context_key}' for"
|
||||
f" '{field_name}' references"
|
||||
f" 'target', but '{context_key}'"
|
||||
" does not allow 'target'."
|
||||
f" Allowed types: {allowed_types}"
|
||||
)
|
||||
|
||||
if not field_exists:
|
||||
raise vol.Invalid(
|
||||
f"Context reference '{field_ref}' for key '{context_key}' does not exist "
|
||||
f"in trigger schema fields or target"
|
||||
f"Context reference '{field_ref}'"
|
||||
f" for key '{context_key}' does"
|
||||
" not exist in trigger schema"
|
||||
" fields or target"
|
||||
)
|
||||
|
||||
return trigger_schema
|
||||
@@ -98,9 +116,9 @@ FIELD_SCHEMA = vol.Schema(
|
||||
vol.Optional("default"): exists,
|
||||
vol.Optional("required"): bool,
|
||||
vol.Optional(CONF_SELECTOR): selector.validate_selector,
|
||||
vol.Optional("context"): {
|
||||
str: str # key is context key, value is field name in the schema which value should be used
|
||||
}, # Will be validated in validate_field_schema
|
||||
# key is context key, value is field name in schema
|
||||
# Validated in validate_field_schema
|
||||
vol.Optional("context"): {str: str},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -271,7 +289,14 @@ def validate_triggers(config: Config, integration: Integration) -> None: # noqa
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"triggers",
|
||||
f"Trigger {trigger_name} has a field {field_name} with a selector with a translation key {translation_key} that is not in the translations file",
|
||||
f"Trigger {trigger_name}"
|
||||
f" has a field"
|
||||
f" {field_name} with a"
|
||||
" selector with a"
|
||||
" translation key"
|
||||
f" {translation_key}"
|
||||
" that is not in the"
|
||||
" translations file",
|
||||
)
|
||||
|
||||
# The same check is done for the description in each of the sections of the
|
||||
@@ -286,7 +311,10 @@ def validate_triggers(config: Config, integration: Integration) -> None: # noqa
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"triggers",
|
||||
f"Trigger {trigger_name} has a section {section_name} with no name {error_msg_suffix}",
|
||||
f"Trigger {trigger_name}"
|
||||
f" has a section"
|
||||
f" {section_name} with no"
|
||||
f" name {error_msg_suffix}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -99,7 +99,8 @@ def main() -> int:
|
||||
if args.template != "integration":
|
||||
generate.generate(args.template, info)
|
||||
|
||||
# Always output sub commands as the output will contain useful information if a command fails.
|
||||
# Always output sub commands as the output will contain
|
||||
# useful information if a command fails.
|
||||
print("Running hassfest to pick up new information.")
|
||||
run_process(
|
||||
"hassfest",
|
||||
@@ -110,7 +111,8 @@ def main() -> int:
|
||||
"--integration-path",
|
||||
str(info.integration_dir),
|
||||
"--skip-plugins",
|
||||
"quality_scale", # Skip quality scale as it will fail for newly generated integrations.
|
||||
# Skip quality scale; it fails for newly generated integrations.
|
||||
"quality_scale",
|
||||
],
|
||||
info,
|
||||
)
|
||||
|
||||
+15
-3
@@ -42,12 +42,21 @@ DATA = {
|
||||
"reproduce_state": {
|
||||
"title": "Reproduce State",
|
||||
"docs": "https://developers.home-assistant.io/docs/core/platform/reproduce_state",
|
||||
"extra": "You will now need to update the code to make sure that every attribute that can occur in the state will cause the right service to be called.",
|
||||
"extra": (
|
||||
"You will now need to update the code to"
|
||||
" make sure that every attribute that can"
|
||||
" occur in the state will cause the right"
|
||||
" service to be called."
|
||||
),
|
||||
},
|
||||
"significant_change": {
|
||||
"title": "Significant Change",
|
||||
"docs": "https://developers.home-assistant.io/docs/core/platform/significant_change",
|
||||
"extra": "You will now need to update the code to make sure that entities with different device classes are correctly considered.",
|
||||
"extra": (
|
||||
"You will now need to update the code to"
|
||||
" make sure that entities with different"
|
||||
" device classes are correctly considered."
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -76,7 +85,10 @@ def print_relevant_docs(template: str, info: Info) -> None:
|
||||
|
||||
if info.examples_added:
|
||||
print(
|
||||
"Because some files already existed, we added the following example files. Please copy the relevant code to the existing files."
|
||||
"Because some files already existed, we"
|
||||
" added the following example files."
|
||||
" Please copy the relevant code to the"
|
||||
" existing files."
|
||||
)
|
||||
for file in info.examples_added:
|
||||
print(f"- {file}")
|
||||
|
||||
@@ -29,7 +29,8 @@ def gather_info(arguments) -> Info:
|
||||
"""What is the domain?
|
||||
|
||||
Hint: The domain is a short name consisting of characters and underscores.
|
||||
This domain has to be unique, cannot be changed, and has to match the directory name of the integration."""
|
||||
This domain has to be unique, cannot be changed,
|
||||
and has to match the directory name of the integration."""
|
||||
),
|
||||
"validators": [
|
||||
CHECK_EMPTY,
|
||||
@@ -79,10 +80,14 @@ def gather_new_integration(determine_auth: bool) -> Info:
|
||||
"codeowner": {
|
||||
"prompt": "What is your GitHub handle?",
|
||||
"validators": [CHECK_EMPTY],
|
||||
"converter": lambda value: value if value.startswith("@") else f"@{value}",
|
||||
"converter": (
|
||||
lambda value: value if value.startswith("@") else f"@{value}"
|
||||
),
|
||||
},
|
||||
"requirement": {
|
||||
"prompt": "What PyPI package and version do you depend on? Leave blank for none.",
|
||||
"prompt": (
|
||||
"What PyPI package and version do you depend on? Leave blank for none."
|
||||
),
|
||||
"validators": [
|
||||
[
|
||||
"Versions should be pinned using '=='.",
|
||||
@@ -112,33 +117,50 @@ More info @ https://developers.home-assistant.io/docs/creating_integration_manif
|
||||
fields.update(
|
||||
{
|
||||
"authentication": {
|
||||
"prompt": "Does Home Assistant need the user to authenticate to control the device/service? (yes/no)",
|
||||
"prompt": (
|
||||
"Does Home Assistant need the user"
|
||||
" to authenticate to control the"
|
||||
" device/service? (yes/no)"
|
||||
),
|
||||
"default": "yes",
|
||||
**YES_NO,
|
||||
},
|
||||
"discoverable": {
|
||||
"prompt": "Is the device/service discoverable on the local network? (yes/no)",
|
||||
"prompt": (
|
||||
"Is the device/service discoverable"
|
||||
" on the local network? (yes/no)"
|
||||
),
|
||||
"default": "no",
|
||||
**YES_NO,
|
||||
},
|
||||
"integration_type": {
|
||||
"prompt": f"""What is the integration type?
|
||||
|
||||
Valid types are {", ".join(IntegrationType)}.
|
||||
This field is recommended and required in some cases. To intentionally leave it unset, type 'omit'.
|
||||
|
||||
More info @ https://developers.home-assistant.io/docs/creating_integration_manifest/#integration-type
|
||||
""",
|
||||
"prompt": (
|
||||
"What is the integration type?"
|
||||
"\n\nValid types are "
|
||||
f"{', '.join(IntegrationType)}."
|
||||
"\nThis field is recommended and"
|
||||
" required in some cases."
|
||||
" To intentionally leave it"
|
||||
" unset, type 'omit'."
|
||||
"\n\nMore info @ "
|
||||
"https://developers.home-assistant.io"
|
||||
"/docs/creating_integration_manifest"
|
||||
"/#integration-type\n"
|
||||
),
|
||||
"validators": [
|
||||
[
|
||||
f"You need to pick one of {', '.join(IntegrationType)} or 'omit'.",
|
||||
"You need to pick one of"
|
||||
f" {', '.join(IntegrationType)}"
|
||||
" or 'omit'.",
|
||||
lambda value: value in IntegrationType or value == "omit",
|
||||
]
|
||||
],
|
||||
"converter": lambda value: None if value == "omit" else value,
|
||||
},
|
||||
"oauth2": {
|
||||
"prompt": "Can the user authenticate the device using OAuth2? (yes/no)",
|
||||
"prompt": (
|
||||
"Can the user authenticate the device using OAuth2? (yes/no)"
|
||||
),
|
||||
"default": "no",
|
||||
**YES_NO,
|
||||
},
|
||||
|
||||
+92
-27
@@ -34,9 +34,12 @@ def _generate(src_dir: Path, target_dir: Path, info: Info) -> None:
|
||||
|
||||
target_file = target_dir / source_file.relative_to(src_dir)
|
||||
|
||||
# If the target file exists, create our template as EXAMPLE_<filename>.
|
||||
# Exception: If we are creating a new integration, we can end up running integration base
|
||||
# and a config flows on top of one another. In that case, we want to override the files.
|
||||
# If the target file exists, create our template as
|
||||
# EXAMPLE_<filename>.
|
||||
# Exception: If we are creating a new integration,
|
||||
# we can end up running integration base and a config
|
||||
# flows on top of one another. In that case, we want
|
||||
# to override the files.
|
||||
if not info.is_new and target_file.exists():
|
||||
new_name = f"EXAMPLE_{target_file.name}"
|
||||
print(f"File {target_file} already exists, creating {new_name} instead.")
|
||||
@@ -127,12 +130,18 @@ def _custom_tasks(template, info: Info) -> None:
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"cannot_connect": (
|
||||
"[%key:common::config_flow::error::cannot_connect%]"
|
||||
),
|
||||
"invalid_auth": (
|
||||
"[%key:common::config_flow::error::invalid_auth%]"
|
||||
),
|
||||
"unknown": ("[%key:common::config_flow::error::unknown%]"),
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": (
|
||||
"[%key:common::config_flow::abort::already_configured_device%]"
|
||||
)
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -143,24 +152,36 @@ def _custom_tasks(template, info: Info) -> None:
|
||||
config={
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]",
|
||||
"description": (
|
||||
"[%key:common::config_flow::description::confirm_setup%]"
|
||||
),
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"single_instance_allowed": (
|
||||
"[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
),
|
||||
"no_devices_found": (
|
||||
"[%key:common::config_flow::abort::no_devices_found%]"
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
elif template == "config_flow_helper":
|
||||
info.update_manifest(config_flow=True, integration_type="helper")
|
||||
info.update_manifest(
|
||||
config_flow=True,
|
||||
integration_type="helper",
|
||||
)
|
||||
info.update_strings(
|
||||
config={
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "New NEW_NAME Sensor",
|
||||
"data": {"entity_id": "Input sensor", "name": "Name"},
|
||||
"data": {
|
||||
"entity_id": "Input sensor",
|
||||
"name": "Name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -168,7 +189,12 @@ def _custom_tasks(template, info: Info) -> None:
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"entity_id": "[%key:component::NEW_DOMAIN::config::step::user::description%]"
|
||||
"entity_id": (
|
||||
"[%key:component"
|
||||
"::NEW_DOMAIN"
|
||||
"::config::step::user"
|
||||
"::description%]"
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -176,29 +202,68 @@ def _custom_tasks(template, info: Info) -> None:
|
||||
)
|
||||
|
||||
elif template == "config_flow_oauth2":
|
||||
info.update_manifest(config_flow=True, dependencies=["application_credentials"])
|
||||
info.update_manifest(
|
||||
config_flow=True,
|
||||
dependencies=["application_credentials"],
|
||||
)
|
||||
info.update_strings(
|
||||
config={
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||
"title": (
|
||||
"[%key:common::config_flow"
|
||||
"::title"
|
||||
"::oauth2_pick_implementation%]"
|
||||
)
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"oauth_implementation_unavailable": "[%key:common::config_flow::abort::oauth2_implementation_unavailable%]",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
||||
"already_configured": (
|
||||
"[%key:common::config_flow::abort::already_configured_account%]"
|
||||
),
|
||||
"already_in_progress": (
|
||||
"[%key:common::config_flow::abort::already_in_progress%]"
|
||||
),
|
||||
"oauth_error": ("[%key:common::config_flow::abort::oauth2_error%]"),
|
||||
"oauth_failed": (
|
||||
"[%key:common::config_flow::abort::oauth2_failed%]"
|
||||
),
|
||||
"oauth_implementation_unavailable": (
|
||||
"[%key:common::config_flow"
|
||||
"::abort"
|
||||
"::oauth2_implementation"
|
||||
"_unavailable%]"
|
||||
),
|
||||
"oauth_timeout": (
|
||||
"[%key:common::config_flow::abort::oauth2_timeout%]"
|
||||
),
|
||||
"oauth_unauthorized": (
|
||||
"[%key:common::config_flow::abort::oauth2_unauthorized%]"
|
||||
),
|
||||
"missing_configuration": (
|
||||
"[%key:common::config_flow"
|
||||
"::abort"
|
||||
"::oauth2_missing_configuration%]"
|
||||
),
|
||||
"authorize_url_timeout": (
|
||||
"[%key:common::config_flow"
|
||||
"::abort"
|
||||
"::oauth2_authorize_url_timeout%]"
|
||||
),
|
||||
"no_url_available": (
|
||||
"[%key:common::config_flow::abort::oauth2_no_url_available%]"
|
||||
),
|
||||
"user_rejected_authorize": (
|
||||
"[%key:common::config_flow"
|
||||
"::abort"
|
||||
"::oauth2_user_rejected"
|
||||
"_authorize%]"
|
||||
),
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
"default": (
|
||||
"[%key:common::config_flow::create_entry::authenticated%]"
|
||||
)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ class ConfigEntryAuth(my_pypi_package.AbstractAuth):
|
||||
super().__init__(self.session.token)
|
||||
|
||||
def refresh_tokens(self) -> str:
|
||||
"""Refresh and return new NEW_NAME tokens using Home Assistant OAuth2 session."""
|
||||
"""Refresh and return new NEW_NAME tokens using HA OAuth2 session."""
|
||||
run_coroutine_threadsafe(
|
||||
self.session.async_ensure_token_valid(), self.hass.loop
|
||||
).result()
|
||||
|
||||
@@ -73,7 +73,11 @@ async def test_if_state(hass: HomeAssistant, service_calls: list[ServiceCall]) -
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"some": "is_on - {{ trigger.platform }} - {{ trigger.event.event_type }}"
|
||||
"some": (
|
||||
"is_on"
|
||||
" - {{ trigger.platform }}"
|
||||
" - {{ trigger.event.event_type }}"
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -91,7 +95,11 @@ async def test_if_state(hass: HomeAssistant, service_calls: list[ServiceCall]) -
|
||||
"action": {
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"some": "is_off - {{ trigger.platform }} - {{ trigger.event.event_type }}"
|
||||
"some": (
|
||||
"is_off"
|
||||
" - {{ trigger.platform }}"
|
||||
" - {{ trigger.event.event_type }}"
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -73,10 +73,13 @@ async def test_if_fires_on_state_change(
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"some": (
|
||||
"turn_on - {{ trigger.platform}} - "
|
||||
"{{ trigger.entity_id}} - {{ trigger.from_state.state}} - "
|
||||
"{{ trigger.to_state.state}} - {{ trigger.for }} - "
|
||||
"{{ trigger.id}}"
|
||||
"turn_on"
|
||||
" - {{ trigger.platform}}"
|
||||
" - {{ trigger.entity_id}}"
|
||||
" - {{ trigger.from_state.state}}"
|
||||
" - {{ trigger.to_state.state}}"
|
||||
" - {{ trigger.for }}"
|
||||
" - {{ trigger.id}}"
|
||||
)
|
||||
},
|
||||
},
|
||||
@@ -93,10 +96,13 @@ async def test_if_fires_on_state_change(
|
||||
"service": "test.automation",
|
||||
"data_template": {
|
||||
"some": (
|
||||
"turn_off - {{ trigger.platform}} - "
|
||||
"{{ trigger.entity_id}} - {{ trigger.from_state.state}} - "
|
||||
"{{ trigger.to_state.state}} - {{ trigger.for }} - "
|
||||
"{{ trigger.id}}"
|
||||
"turn_off"
|
||||
" - {{ trigger.platform}}"
|
||||
" - {{ trigger.entity_id}}"
|
||||
" - {{ trigger.from_state.state}}"
|
||||
" - {{ trigger.to_state.state}}"
|
||||
" - {{ trigger.for }}"
|
||||
" - {{ trigger.id}}"
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -89,4 +89,7 @@ async def async_reproduce_states(
|
||||
|
||||
# Alternative: Reproduce states in sequence
|
||||
# for state in states:
|
||||
# await _async_reproduce_state(hass, state, context=context, reproduce_options=reproduce_options)
|
||||
# await _async_reproduce_state(
|
||||
# hass, state, context=context,
|
||||
# reproduce_options=reproduce_options,
|
||||
# )
|
||||
|
||||
@@ -65,7 +65,9 @@ class BucketHolder:
|
||||
if other_test is tests or isinstance(other_test, TestFolder):
|
||||
continue
|
||||
print(
|
||||
f"{other_test.total_tests:>{digits}} tests in {other_test.path} (same bucket)"
|
||||
f"{other_test.total_tests:>{digits}}"
|
||||
f" tests in {other_test.path}"
|
||||
" (same bucket)"
|
||||
)
|
||||
smallest_bucket.add(other_test)
|
||||
|
||||
|
||||
@@ -100,7 +100,9 @@ def run():
|
||||
key_data = lokalise.keys_list({"filter_keys": ",".join(chunk), "limit": 1000})
|
||||
if len(key_data) != len(chunk):
|
||||
print(
|
||||
f"Looking up key in Lokalise returns {len(key_data)} results, expected {len(chunk)}"
|
||||
f"Looking up key in Lokalise returns"
|
||||
f" {len(key_data)} results,"
|
||||
f" expected {len(chunk)}"
|
||||
)
|
||||
|
||||
if not key_data:
|
||||
|
||||
@@ -97,14 +97,18 @@ def save_language_translations(lang: str, translations: dict[str, Any]) -> None:
|
||||
component_path = Path("homeassistant") / "components" / component
|
||||
if not component_path.is_dir():
|
||||
print(
|
||||
f"Skipping {lang} for {component}, as the integration doesn't seem to exist."
|
||||
f"Skipping {lang} for {component},"
|
||||
" as the integration doesn't seem"
|
||||
" to exist."
|
||||
)
|
||||
continue
|
||||
|
||||
strings_path = component_path / "strings.json"
|
||||
if not strings_path.exists():
|
||||
print(
|
||||
f"Skipping {lang} for {component}, as the integration doesn't have a strings.json file."
|
||||
f"Skipping {lang} for {component},"
|
||||
" as the integration doesn't have"
|
||||
" a strings.json file."
|
||||
)
|
||||
continue
|
||||
strings = load_json_from_path(strings_path)
|
||||
|
||||
@@ -29,7 +29,9 @@ def rename_keys(project_id, to_migrate):
|
||||
from_key_data = lokalise.keys_list({"filter_keys": ",".join(to_migrate)})
|
||||
if len(from_key_data) != len(to_migrate):
|
||||
print(
|
||||
f"Looking up keys in Lokalise returns {len(from_key_data)} results, expected {len(to_migrate)}"
|
||||
f"Looking up keys in Lokalise returns"
|
||||
f" {len(from_key_data)} results,"
|
||||
f" expected {len(to_migrate)}"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -74,7 +76,9 @@ def list_keys_helper(lokalise, keys, params=None, *, validate=True):
|
||||
continue
|
||||
|
||||
print(
|
||||
f"Looking up keys in Lokalise returns {len(from_key_data)} results, expected {len(keys)}"
|
||||
f"Looking up keys in Lokalise returns"
|
||||
f" {len(from_key_data)} results,"
|
||||
f" expected {len(keys)}"
|
||||
)
|
||||
searched = set(filter_keys)
|
||||
returned = set(create_lookup(from_key_data))
|
||||
@@ -348,7 +352,10 @@ def apply_data_references(to_migrate):
|
||||
pass
|
||||
else:
|
||||
print(
|
||||
f"{strings_file}: Skipped swapping '{key}': '{value}' does not contain '{key}'"
|
||||
f"{strings_file}: Skipped"
|
||||
f" swapping '{key}':"
|
||||
f" '{value}' does not"
|
||||
f" contain '{key}'"
|
||||
)
|
||||
|
||||
if not changed:
|
||||
|
||||
@@ -65,7 +65,8 @@ def run():
|
||||
"""Run the script."""
|
||||
if get_current_branch() != "dev" and os.environ.get("AZURE_BRANCH") != "dev":
|
||||
raise ExitApp(
|
||||
"Please only run the translations upload script from a clean checkout of dev."
|
||||
"Please only run the translations upload"
|
||||
" script from a clean checkout of dev."
|
||||
)
|
||||
|
||||
translations = generate_upload_data()
|
||||
|
||||
@@ -133,7 +133,9 @@ def substitute_references(
|
||||
except MissingReference as err:
|
||||
if fail_on_missing:
|
||||
raise ExitApp(
|
||||
f"Missing reference '{err.reference_key}' in translation for key '{key}'"
|
||||
f"Missing reference"
|
||||
f" '{err.reference_key}'"
|
||||
f" in translation for key '{key}'"
|
||||
) from err
|
||||
continue
|
||||
result[key] = substituted
|
||||
|
||||
Reference in New Issue
Block a user