Fix line length violations in script/ (#170759)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Franck Nijhof
2026-05-17 10:30:02 -04:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 018e3a4765
commit f0396aca8a
30 changed files with 388 additions and 138 deletions
+9 -6
View File
@@ -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
+4 -1
View File
@@ -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",
+2 -1
View File
@@ -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,
)
+2 -1
View File
@@ -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 (
+45 -18
View File
@@ -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}",
)
+4 -3
View File
@@ -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
+9 -3
View File
@@ -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
+2 -1
View File
@@ -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)
+5 -2
View File
@@ -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",
)
+8 -3
View File
@@ -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
+2 -1
View File
@@ -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!")
+22 -7
View File
@@ -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}",
)
+6 -3
View File
@@ -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(
+18 -4
View File
@@ -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
View File
@@ -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}",
)
+4 -2
View File
@@ -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
View File
@@ -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}")
+36 -14
View 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
View File
@@ -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,
# )
+3 -1
View File
@@ -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)
+3 -1
View File
@@ -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:
+6 -2
View File
@@ -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)
+10 -3
View File
@@ -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:
+2 -1
View File
@@ -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()
+3 -1
View File
@@ -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