Fix docker hassfest (#132823)

This commit is contained in:
Robert Resch 2024-12-11 08:55:00 +01:00 committed by GitHub
parent 5e17721568
commit af838077cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 85 additions and 49 deletions

View File

@ -517,7 +517,7 @@ jobs:
tags: ${{ env.HASSFEST_IMAGE_TAG }} tags: ${{ env.HASSFEST_IMAGE_TAG }}
- name: Run hassfest against core - name: Run hassfest against core
run: docker run --rm -v ${{ github.workspace }}/homeassistant:/github/workspace/homeassistant ${{ env.HASSFEST_IMAGE_TAG }} --core-integrations-path=/github/workspace/homeassistant/components run: docker run --rm -v ${{ github.workspace }}:/github/workspace ${{ env.HASSFEST_IMAGE_TAG }} --core-path=/github/workspace
- name: Push Docker image - name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'

View File

@ -628,7 +628,6 @@ def _get_hassfest_config() -> Config:
specific_integrations=None, specific_integrations=None,
action="validate", action="validate",
requirements=True, requirements=True,
core_integrations_path=Path("homeassistant/components"),
) )

View File

@ -110,10 +110,10 @@ def get_config() -> Config:
help="Comma-separate list of plugins to run. Valid plugin names: %(default)s", help="Comma-separate list of plugins to run. Valid plugin names: %(default)s",
) )
parser.add_argument( parser.add_argument(
"--core-integrations-path", "--core-path",
type=Path, type=Path,
default=Path("homeassistant/components"), default=Path(),
help="Path to core integrations", help="Path to core",
) )
parsed = parser.parse_args() parsed = parser.parse_args()
@ -125,16 +125,18 @@ def get_config() -> Config:
"Generate is not allowed when limiting to specific integrations" "Generate is not allowed when limiting to specific integrations"
) )
if not parsed.integration_path and not Path("requirements_all.txt").is_file(): if (
not parsed.integration_path
and not (parsed.core_path / "requirements_all.txt").is_file()
):
raise RuntimeError("Run from Home Assistant root") raise RuntimeError("Run from Home Assistant root")
return Config( return Config(
root=Path().absolute(), root=parsed.core_path.absolute(),
specific_integrations=parsed.integration_path, specific_integrations=parsed.integration_path,
action=parsed.action, action=parsed.action,
requirements=parsed.requirements, requirements=parsed.requirements,
plugins=set(parsed.plugins), plugins=set(parsed.plugins),
core_integrations_path=parsed.core_integrations_path,
) )

View File

@ -185,12 +185,12 @@ def _generate_files(config: Config) -> list[File]:
+ 10 + 10
) * 1000 ) * 1000
package_versions = _get_package_versions(Path("requirements.txt"), {"uv"}) package_versions = _get_package_versions(config.root / "requirements.txt", {"uv"})
package_versions |= _get_package_versions( package_versions |= _get_package_versions(
Path("requirements_test.txt"), {"pipdeptree", "tqdm"} config.root / "requirements_test.txt", {"pipdeptree", "tqdm"}
) )
package_versions |= _get_package_versions( package_versions |= _get_package_versions(
Path("requirements_test_pre_commit.txt"), {"ruff"} config.root / "requirements_test_pre_commit.txt", {"ruff"}
) )
return [ return [

View File

@ -2,16 +2,28 @@
integrations="" integrations=""
integration_path="" integration_path=""
core_path_provided=false
# Enable recursive globbing using find for arg in "$@"; do
for manifest in $(find . -name "manifest.json"); do case "$arg" in
manifest_path=$(realpath "${manifest}") --core-path=*)
integrations="$integrations --integration-path ${manifest_path%/*}" core_path_provided=true
break
;;
esac
done done
if [ -z "$integrations" ]; then if [ "$core_path_provided" = false ]; then
# Enable recursive globbing using find
for manifest in $(find . -name "manifest.json"); do
manifest_path=$(realpath "${manifest}")
integrations="$integrations --integration-path ${manifest_path%/*}"
done
if [ -z "$integrations" ]; then
echo "Error: No integrations found!" echo "Error: No integrations found!"
exit 1 exit 1
fi
fi fi
cd /usr/src/homeassistant || exit 1 cd /usr/src/homeassistant || exit 1

View File

@ -30,11 +30,15 @@ class Config:
root: pathlib.Path root: pathlib.Path
action: Literal["validate", "generate"] action: Literal["validate", "generate"]
requirements: bool requirements: bool
core_integrations_path: pathlib.Path core_integrations_path: pathlib.Path = field(init=False)
errors: list[Error] = field(default_factory=list) errors: list[Error] = field(default_factory=list)
cache: dict[str, Any] = field(default_factory=dict) cache: dict[str, Any] = field(default_factory=dict)
plugins: set[str] = field(default_factory=set) plugins: set[str] = field(default_factory=set)
def __post_init__(self) -> None:
"""Post init."""
self.core_integrations_path = self.root / "homeassistant/components"
def add_error(self, *args: Any, **kwargs: Any) -> None: def add_error(self, *args: Any, **kwargs: Any) -> None:
"""Add an error.""" """Add an error."""
self.errors.append(Error(*args, **kwargs)) self.errors.append(Error(*args, **kwargs))

View File

@ -1358,7 +1358,7 @@ def validate_iqs_file(config: Config, integration: Integration) -> None:
for rule_name in rules_done: for rule_name in rules_done:
if (validator := VALIDATORS.get(rule_name)) and ( if (validator := VALIDATORS.get(rule_name)) and (
errors := validator.validate(integration, rules_done=rules_done) errors := validator.validate(config, integration, rules_done=rules_done)
): ):
for error in errors: for error in errors:
integration.add_error("quality_scale", f"[{rule_name}] {error}") integration.add_error("quality_scale", f"[{rule_name}] {error}")

View File

@ -2,14 +2,14 @@
from typing import Protocol from typing import Protocol
from script.hassfest.model import Integration from script.hassfest.model import Config, Integration
class RuleValidationProtocol(Protocol): class RuleValidationProtocol(Protocol):
"""Protocol for rule validation.""" """Protocol for rule validation."""
def validate( def validate(
self, integration: Integration, *, rules_done: set[str] self, config: Config, integration: Integration, *, rules_done: set[str]
) -> list[str] | None: ) -> list[str] | None:
"""Validate a quality scale rule. """Validate a quality scale rule.

View File

@ -6,7 +6,7 @@ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/c
import ast import ast
from script.hassfest import ast_parse_module from script.hassfest import ast_parse_module
from script.hassfest.model import Integration from script.hassfest.model import Config, Integration
def _has_unload_entry_function(module: ast.Module) -> bool: def _has_unload_entry_function(module: ast.Module) -> bool:
@ -17,7 +17,9 @@ def _has_unload_entry_function(module: ast.Module) -> bool:
) )
def validate(integration: Integration, *, rules_done: set[str]) -> list[str] | None: def validate(
config: Config, integration: Integration, *, rules_done: set[str]
) -> list[str] | None:
"""Validate that the integration has a config flow.""" """Validate that the integration has a config flow."""
init_file = integration.path / "__init__.py" init_file = integration.path / "__init__.py"

View File

@ -3,10 +3,12 @@
https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/config-flow/ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/config-flow/
""" """
from script.hassfest.model import Integration from script.hassfest.model import Config, Integration
def validate(integration: Integration, *, rules_done: set[str]) -> list[str] | None: def validate(
config: Config, integration: Integration, *, rules_done: set[str]
) -> list[str] | None:
"""Validate that the integration implements config flow.""" """Validate that the integration implements config flow."""
if not integration.config_flow: if not integration.config_flow:

View File

@ -6,7 +6,7 @@ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/d
import ast import ast
from script.hassfest import ast_parse_module from script.hassfest import ast_parse_module
from script.hassfest.model import Integration from script.hassfest.model import Config, Integration
DIAGNOSTICS_FUNCTIONS = { DIAGNOSTICS_FUNCTIONS = {
"async_get_config_entry_diagnostics", "async_get_config_entry_diagnostics",
@ -22,7 +22,9 @@ def _has_diagnostics_function(module: ast.Module) -> bool:
) )
def validate(integration: Integration, *, rules_done: set[str]) -> list[str] | None: def validate(
config: Config, integration: Integration, *, rules_done: set[str]
) -> list[str] | None:
"""Validate that the integration implements diagnostics.""" """Validate that the integration implements diagnostics."""
diagnostics_file = integration.path / "diagnostics.py" diagnostics_file = integration.path / "diagnostics.py"

View File

@ -6,7 +6,7 @@ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/d
import ast import ast
from script.hassfest import ast_parse_module from script.hassfest import ast_parse_module
from script.hassfest.model import Integration from script.hassfest.model import Config, Integration
MANIFEST_KEYS = [ MANIFEST_KEYS = [
"bluetooth", "bluetooth",
@ -38,7 +38,9 @@ def _has_discovery_function(module: ast.Module) -> bool:
) )
def validate(integration: Integration, *, rules_done: set[str]) -> list[str] | None: def validate(
config: Config, integration: Integration, *, rules_done: set[str]
) -> list[str] | None:
"""Validate that the integration implements diagnostics.""" """Validate that the integration implements diagnostics."""
config_flow_file = integration.path / "config_flow.py" config_flow_file = integration.path / "config_flow.py"

View File

@ -7,7 +7,7 @@ import ast
from homeassistant.const import Platform from homeassistant.const import Platform
from script.hassfest import ast_parse_module from script.hassfest import ast_parse_module
from script.hassfest.model import Integration from script.hassfest.model import Config, Integration
def _has_parallel_updates_defined(module: ast.Module) -> bool: def _has_parallel_updates_defined(module: ast.Module) -> bool:
@ -18,7 +18,9 @@ def _has_parallel_updates_defined(module: ast.Module) -> bool:
) )
def validate(integration: Integration, *, rules_done: set[str]) -> list[str] | None: def validate(
config: Config, integration: Integration, *, rules_done: set[str]
) -> list[str] | None:
"""Validate that the integration sets PARALLEL_UPDATES constant.""" """Validate that the integration sets PARALLEL_UPDATES constant."""
errors = [] errors = []

View File

@ -6,7 +6,7 @@ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/r
import ast import ast
from script.hassfest import ast_parse_module from script.hassfest import ast_parse_module
from script.hassfest.model import Integration from script.hassfest.model import Config, Integration
def _has_step_reauth_function(module: ast.Module) -> bool: def _has_step_reauth_function(module: ast.Module) -> bool:
@ -17,7 +17,9 @@ def _has_step_reauth_function(module: ast.Module) -> bool:
) )
def validate(integration: Integration, *, rules_done: set[str]) -> list[str] | None: def validate(
config: Config, integration: Integration, *, rules_done: set[str]
) -> list[str] | None:
"""Validate that the integration has a reauthentication flow.""" """Validate that the integration has a reauthentication flow."""
config_flow_file = integration.path / "config_flow.py" config_flow_file = integration.path / "config_flow.py"

View File

@ -6,7 +6,7 @@ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/r
import ast import ast
from script.hassfest import ast_parse_module from script.hassfest import ast_parse_module
from script.hassfest.model import Integration from script.hassfest.model import Config, Integration
def _has_step_reconfigure_function(module: ast.Module) -> bool: def _has_step_reconfigure_function(module: ast.Module) -> bool:
@ -17,7 +17,9 @@ def _has_step_reconfigure_function(module: ast.Module) -> bool:
) )
def validate(integration: Integration, *, rules_done: set[str]) -> list[str] | None: def validate(
config: Config, integration: Integration, *, rules_done: set[str]
) -> list[str] | None:
"""Validate that the integration has a reconfiguration flow.""" """Validate that the integration has a reconfiguration flow."""
config_flow_file = integration.path / "config_flow.py" config_flow_file = integration.path / "config_flow.py"

View File

@ -8,7 +8,7 @@ import re
from homeassistant.const import Platform from homeassistant.const import Platform
from script.hassfest import ast_parse_module from script.hassfest import ast_parse_module
from script.hassfest.model import Integration from script.hassfest.model import Config, Integration
_ANNOTATION_MATCH = re.compile(r"^[A-Za-z]+ConfigEntry$") _ANNOTATION_MATCH = re.compile(r"^[A-Za-z]+ConfigEntry$")
_FUNCTIONS: dict[str, dict[str, int]] = { _FUNCTIONS: dict[str, dict[str, int]] = {
@ -102,7 +102,9 @@ def _check_typed_config_entry(integration: Integration) -> list[str]:
return errors return errors
def validate(integration: Integration, *, rules_done: set[str]) -> list[str] | None: def validate(
config: Config, integration: Integration, *, rules_done: set[str]
) -> list[str] | None:
"""Validate correct use of ConfigEntry.runtime_data.""" """Validate correct use of ConfigEntry.runtime_data."""
init_file = integration.path / "__init__.py" init_file = integration.path / "__init__.py"
init = ast_parse_module(init_file) init = ast_parse_module(init_file)

View File

@ -7,27 +7,30 @@ from functools import lru_cache
from pathlib import Path from pathlib import Path
import re import re
from script.hassfest.model import Integration from script.hassfest.model import Config, Integration
_STRICT_TYPING_FILE = Path(".strict-typing") _STRICT_TYPING_FILE = Path(".strict-typing")
_COMPONENT_REGEX = r"homeassistant.components.([^.]+).*" _COMPONENT_REGEX = r"homeassistant.components.([^.]+).*"
@lru_cache @lru_cache
def _strict_typing_components() -> set[str]: def _strict_typing_components(strict_typing_file: Path) -> set[str]:
return set( return set(
{ {
match.group(1) match.group(1)
for line in _STRICT_TYPING_FILE.read_text(encoding="utf-8").splitlines() for line in strict_typing_file.read_text(encoding="utf-8").splitlines()
if (match := re.match(_COMPONENT_REGEX, line)) is not None if (match := re.match(_COMPONENT_REGEX, line)) is not None
} }
) )
def validate(integration: Integration, *, rules_done: set[str]) -> list[str] | None: def validate(
config: Config, integration: Integration, *, rules_done: set[str]
) -> list[str] | None:
"""Validate that the integration has strict typing enabled.""" """Validate that the integration has strict typing enabled."""
strict_typing_file = config.root / _STRICT_TYPING_FILE
if integration.domain not in _strict_typing_components(): if integration.domain not in _strict_typing_components(strict_typing_file):
return [ return [
"Integration does not have strict typing enabled " "Integration does not have strict typing enabled "
"(is missing from .strict-typing)" "(is missing from .strict-typing)"

View File

@ -6,7 +6,7 @@ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/u
import ast import ast
from script.hassfest import ast_parse_module from script.hassfest import ast_parse_module
from script.hassfest.model import Integration from script.hassfest.model import Config, Integration
def _has_method_call(module: ast.Module, name: str) -> bool: def _has_method_call(module: ast.Module, name: str) -> bool:
@ -30,7 +30,9 @@ def _has_abort_unique_id_configured(module: ast.Module) -> bool:
) )
def validate(integration: Integration, *, rules_done: set[str]) -> list[str] | None: def validate(
config: Config, integration: Integration, *, rules_done: set[str]
) -> list[str] | None:
"""Validate that the integration prevents duplicate devices.""" """Validate that the integration prevents duplicate devices."""
if integration.manifest.get("single_config_entry"): if integration.manifest.get("single_config_entry"):

View File

@ -12,13 +12,12 @@ from script.hassfest.requirements import validate_requirements_format
def integration(): def integration():
"""Fixture for hassfest integration model.""" """Fixture for hassfest integration model."""
return Integration( return Integration(
path=Path("homeassistant/components/test"), path=Path("homeassistant/components/test").absolute(),
_config=Config( _config=Config(
root=Path(".").absolute(), root=Path(".").absolute(),
specific_integrations=None, specific_integrations=None,
action="validate", action="validate",
requirements=True, requirements=True,
core_integrations_path=Path("homeassistant/components"),
), ),
_manifest={ _manifest={
"domain": "test", "domain": "test",

View File

@ -16,13 +16,12 @@ from script.hassfest.model import Config, Integration
def integration(): def integration():
"""Fixture for hassfest integration model.""" """Fixture for hassfest integration model."""
integration = Integration( integration = Integration(
"", Path(),
_config=Config( _config=Config(
root=Path(".").absolute(), root=Path(".").absolute(),
specific_integrations=None, specific_integrations=None,
action="validate", action="validate",
requirements=True, requirements=True,
core_integrations_path=Path("homeassistant/components"),
), ),
) )
integration._manifest = { integration._manifest = {