mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Validate quality scale tiers against the tier declared in the integration manifest (#131286)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
This commit is contained in:
parent
0626b005e2
commit
96e67373db
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import IntEnum, StrEnum, auto
|
from enum import StrEnum, auto
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -20,7 +20,7 @@ from voluptuous.humanize import humanize_error
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
from .model import Config, Integration
|
from .model import Config, Integration, ScaledQualityScaleTiers
|
||||||
|
|
||||||
DOCUMENTATION_URL_SCHEMA = "https"
|
DOCUMENTATION_URL_SCHEMA = "https"
|
||||||
DOCUMENTATION_URL_HOST = "www.home-assistant.io"
|
DOCUMENTATION_URL_HOST = "www.home-assistant.io"
|
||||||
@ -28,15 +28,6 @@ DOCUMENTATION_URL_PATH_PREFIX = "/integrations/"
|
|||||||
DOCUMENTATION_URL_EXCEPTIONS = {"https://www.home-assistant.io/hassio"}
|
DOCUMENTATION_URL_EXCEPTIONS = {"https://www.home-assistant.io/hassio"}
|
||||||
|
|
||||||
|
|
||||||
class ScaledQualityScaleTiers(IntEnum):
|
|
||||||
"""Supported manifest quality scales."""
|
|
||||||
|
|
||||||
BRONZE = 1
|
|
||||||
SILVER = 2
|
|
||||||
GOLD = 3
|
|
||||||
PLATINUM = 4
|
|
||||||
|
|
||||||
|
|
||||||
class NonScaledQualityScaleTiers(StrEnum):
|
class NonScaledQualityScaleTiers(StrEnum):
|
||||||
"""Supported manifest quality scales."""
|
"""Supported manifest quality scales."""
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from enum import IntEnum
|
||||||
import json
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
@ -230,3 +231,12 @@ class Integration:
|
|||||||
|
|
||||||
self._manifest = manifest
|
self._manifest = manifest
|
||||||
self.manifest_path = manifest_path
|
self.manifest_path = manifest_path
|
||||||
|
|
||||||
|
|
||||||
|
class ScaledQualityScaleTiers(IntEnum):
|
||||||
|
"""Supported manifest quality scales."""
|
||||||
|
|
||||||
|
BRONZE = 1
|
||||||
|
SILVER = 2
|
||||||
|
GOLD = 3
|
||||||
|
PLATINUM = 4
|
||||||
|
@ -9,32 +9,52 @@ from homeassistant.const import Platform
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.util.yaml import load_yaml_dict
|
from homeassistant.util.yaml import load_yaml_dict
|
||||||
|
|
||||||
from .model import Config, Integration
|
from .model import Config, Integration, ScaledQualityScaleTiers
|
||||||
|
|
||||||
RULES = [
|
QUALITY_SCALE_TIERS = {value.name.lower(): value for value in ScaledQualityScaleTiers}
|
||||||
"action-exceptions",
|
|
||||||
|
RULES = {
|
||||||
|
ScaledQualityScaleTiers.BRONZE: [
|
||||||
"action-setup",
|
"action-setup",
|
||||||
"appropriate-polling",
|
"appropriate-polling",
|
||||||
"async-dependency",
|
|
||||||
"brands",
|
"brands",
|
||||||
"common-modules",
|
"common-modules",
|
||||||
"config-entry-unloading",
|
|
||||||
"config-flow",
|
"config-flow",
|
||||||
"config-flow-test-coverage",
|
"config-flow-test-coverage",
|
||||||
"dependency-transparency",
|
"dependency-transparency",
|
||||||
|
"docs-actions",
|
||||||
|
"docs-high-level-description",
|
||||||
|
"docs-installation-parameters",
|
||||||
|
"docs-installation-instructions",
|
||||||
|
"docs-removal-instructions",
|
||||||
|
"entity-event-setup",
|
||||||
|
"entity-unique-id",
|
||||||
|
"has-entity-name",
|
||||||
|
"runtime-data",
|
||||||
|
"test-before-configure",
|
||||||
|
"test-before-setup",
|
||||||
|
"unique-config-entry",
|
||||||
|
],
|
||||||
|
ScaledQualityScaleTiers.SILVER: [
|
||||||
|
"action-exceptions",
|
||||||
|
"config-entry-unloading",
|
||||||
|
"docs-configuration-parameters",
|
||||||
|
"docs-installation-parameters",
|
||||||
|
"entity-unavailable",
|
||||||
|
"integration-owner",
|
||||||
|
"log-when-unavailable",
|
||||||
|
"parallel-updates",
|
||||||
|
"reauthentication-flow",
|
||||||
|
"test-coverage",
|
||||||
|
],
|
||||||
|
ScaledQualityScaleTiers.GOLD: [
|
||||||
"devices",
|
"devices",
|
||||||
"diagnostics",
|
"diagnostics",
|
||||||
"discovery",
|
"discovery",
|
||||||
"discovery-update-info",
|
"discovery-update-info",
|
||||||
"docs-actions",
|
|
||||||
"docs-configuration-parameters",
|
|
||||||
"docs-data-update",
|
"docs-data-update",
|
||||||
"docs-examples",
|
"docs-examples",
|
||||||
"docs-high-level-description",
|
|
||||||
"docs-installation-instructions",
|
|
||||||
"docs-installation-parameters",
|
|
||||||
"docs-known-limitations",
|
"docs-known-limitations",
|
||||||
"docs-removal-instructions",
|
|
||||||
"docs-supported-devices",
|
"docs-supported-devices",
|
||||||
"docs-supported-functions",
|
"docs-supported-functions",
|
||||||
"docs-troubleshooting",
|
"docs-troubleshooting",
|
||||||
@ -43,28 +63,19 @@ RULES = [
|
|||||||
"entity-category",
|
"entity-category",
|
||||||
"entity-device-class",
|
"entity-device-class",
|
||||||
"entity-disabled-by-default",
|
"entity-disabled-by-default",
|
||||||
"entity-event-setup",
|
|
||||||
"entity-translations",
|
"entity-translations",
|
||||||
"entity-unavailable",
|
|
||||||
"entity-unique-id",
|
|
||||||
"exception-translations",
|
"exception-translations",
|
||||||
"has-entity-name",
|
|
||||||
"icon-translations",
|
"icon-translations",
|
||||||
"inject-websession",
|
|
||||||
"integration-owner",
|
|
||||||
"log-when-unavailable",
|
|
||||||
"parallel-updates",
|
|
||||||
"reauthentication-flow",
|
|
||||||
"reconfiguration-flow",
|
"reconfiguration-flow",
|
||||||
"repair-issues",
|
"repair-issues",
|
||||||
"runtime-data",
|
|
||||||
"stale-devices",
|
"stale-devices",
|
||||||
|
],
|
||||||
|
ScaledQualityScaleTiers.PLATINUM: [
|
||||||
|
"async-dependency",
|
||||||
|
"inject-websession",
|
||||||
"strict-typing",
|
"strict-typing",
|
||||||
"test-before-configure",
|
],
|
||||||
"test-before-setup",
|
}
|
||||||
"test-coverage",
|
|
||||||
"unique-config-entry",
|
|
||||||
]
|
|
||||||
|
|
||||||
INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||||
"abode",
|
"abode",
|
||||||
@ -1264,7 +1275,8 @@ SCHEMA = vol.Schema(
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for rule in RULES
|
for tier_list in RULES.values()
|
||||||
|
for rule in tier_list
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1275,6 +1287,9 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
|
|||||||
"""Validate quality scale file for integration."""
|
"""Validate quality scale file for integration."""
|
||||||
if not integration.core:
|
if not integration.core:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
declared_quality_scale = QUALITY_SCALE_TIERS.get(integration.quality_scale)
|
||||||
|
|
||||||
iqs_file = integration.path / "quality_scale.yaml"
|
iqs_file = integration.path / "quality_scale.yaml"
|
||||||
has_file = iqs_file.is_file()
|
has_file = iqs_file.is_file()
|
||||||
if not has_file:
|
if not has_file:
|
||||||
@ -1288,6 +1303,12 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
|
|||||||
"Quality scale definition not found. New integrations are required to at least reach the Bronze tier.",
|
"Quality scale definition not found. New integrations are required to at least reach the Bronze tier.",
|
||||||
)
|
)
|
||||||
return
|
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.",
|
||||||
|
)
|
||||||
|
return
|
||||||
return
|
return
|
||||||
if integration.integration_type == "virtual":
|
if integration.integration_type == "virtual":
|
||||||
integration.add_error(
|
integration.add_error(
|
||||||
@ -1322,6 +1343,28 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
|
|||||||
"quality_scale", f"Invalid {name}: {humanize_error(data, err)}"
|
"quality_scale", f"Invalid {name}: {humanize_error(data, err)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if declared_quality_scale is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
rules_met = set()
|
||||||
|
for rule_name, rule_value in data.get("rules", {}).items():
|
||||||
|
status = rule_value["status"] if isinstance(rule_value, dict) else rule_value
|
||||||
|
if status in {"done", "exempt"}:
|
||||||
|
rules_met.add(rule_name)
|
||||||
|
|
||||||
|
# An integration must have all the necessary rules for the declared
|
||||||
|
# quality scale, and all the rules below.
|
||||||
|
for scale in ScaledQualityScaleTiers:
|
||||||
|
if scale > declared_quality_scale:
|
||||||
|
break
|
||||||
|
required_rules = set(RULES[scale])
|
||||||
|
if missing_rules := (required_rules - rules_met):
|
||||||
|
friendly_rule_str = "\n".join(f" {rule}: todo" for rule in missing_rules)
|
||||||
|
integration.add_error(
|
||||||
|
"quality_scale",
|
||||||
|
f"Quality scale tier {scale.name.lower()} requires quality scale rules to be met:\n{friendly_rule_str}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate(integrations: dict[str, Integration], config: Config) -> None:
|
def validate(integrations: dict[str, Integration], config: Config) -> None:
|
||||||
"""Handle YAML files inside integrations."""
|
"""Handle YAML files inside integrations."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user