diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 9d3ab18f7c1..8f419cca1da 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -517,7 +517,7 @@ jobs: tags: ${{ env.HASSFEST_IMAGE_TAG }} - 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 if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fa46710d100..5cc609eec2a 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -628,7 +628,6 @@ def _get_hassfest_config() -> Config: specific_integrations=None, action="validate", requirements=True, - core_integrations_path=Path("homeassistant/components"), ) diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 81670de5afd..c93d8fd4499 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -110,10 +110,10 @@ def get_config() -> Config: help="Comma-separate list of plugins to run. Valid plugin names: %(default)s", ) parser.add_argument( - "--core-integrations-path", + "--core-path", type=Path, - default=Path("homeassistant/components"), - help="Path to core integrations", + default=Path(), + help="Path to core", ) parsed = parser.parse_args() @@ -125,16 +125,18 @@ def get_config() -> Config: "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") return Config( - root=Path().absolute(), + root=parsed.core_path.absolute(), specific_integrations=parsed.integration_path, action=parsed.action, requirements=parsed.requirements, plugins=set(parsed.plugins), - core_integrations_path=parsed.core_integrations_path, ) diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 57d86bc4def..022caee30cd 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -185,12 +185,12 @@ def _generate_files(config: Config) -> list[File]: + 10 ) * 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( - Path("requirements_test.txt"), {"pipdeptree", "tqdm"} + config.root / "requirements_test.txt", {"pipdeptree", "tqdm"} ) package_versions |= _get_package_versions( - Path("requirements_test_pre_commit.txt"), {"ruff"} + config.root / "requirements_test_pre_commit.txt", {"ruff"} ) return [ diff --git a/script/hassfest/docker/entrypoint.sh b/script/hassfest/docker/entrypoint.sh index 7b75eb186d2..eabc08a9499 100755 --- a/script/hassfest/docker/entrypoint.sh +++ b/script/hassfest/docker/entrypoint.sh @@ -2,16 +2,28 @@ integrations="" integration_path="" +core_path_provided=false -# Enable recursive globbing using find -for manifest in $(find . -name "manifest.json"); do - manifest_path=$(realpath "${manifest}") - integrations="$integrations --integration-path ${manifest_path%/*}" +for arg in "$@"; do + case "$arg" in + --core-path=*) + core_path_provided=true + break + ;; + esac done -if [ -z "$integrations" ]; then - echo "Error: No integrations found!" - exit 1 +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!" + exit 1 + fi fi cd /usr/src/homeassistant || exit 1 diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 377f82b0d5c..08ded687096 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -30,11 +30,15 @@ class Config: root: pathlib.Path action: Literal["validate", "generate"] requirements: bool - core_integrations_path: pathlib.Path + core_integrations_path: pathlib.Path = field(init=False) errors: list[Error] = field(default_factory=list) cache: dict[str, Any] = field(default_factory=dict) 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: """Add an error.""" self.errors.append(Error(*args, **kwargs)) diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 72f01f3d1d1..5a09f8c7bd8 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -1358,7 +1358,7 @@ def validate_iqs_file(config: Config, integration: Integration) -> None: for rule_name in rules_done: 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: integration.add_error("quality_scale", f"[{rule_name}] {error}") diff --git a/script/hassfest/quality_scale_validation/__init__.py b/script/hassfest/quality_scale_validation/__init__.py index 892bb70fabd..7c41a58b601 100644 --- a/script/hassfest/quality_scale_validation/__init__.py +++ b/script/hassfest/quality_scale_validation/__init__.py @@ -2,14 +2,14 @@ from typing import Protocol -from script.hassfest.model import Integration +from script.hassfest.model import Config, Integration class RuleValidationProtocol(Protocol): """Protocol for rule validation.""" def validate( - self, integration: Integration, *, rules_done: set[str] + self, config: Config, integration: Integration, *, rules_done: set[str] ) -> list[str] | None: """Validate a quality scale rule. diff --git a/script/hassfest/quality_scale_validation/config_entry_unloading.py b/script/hassfest/quality_scale_validation/config_entry_unloading.py index fb636a7f2ed..4874ddc4625 100644 --- a/script/hassfest/quality_scale_validation/config_entry_unloading.py +++ b/script/hassfest/quality_scale_validation/config_entry_unloading.py @@ -6,7 +6,7 @@ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/c import ast 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: @@ -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.""" init_file = integration.path / "__init__.py" diff --git a/script/hassfest/quality_scale_validation/config_flow.py b/script/hassfest/quality_scale_validation/config_flow.py index 6e88aa462f4..d1ac70ab469 100644 --- a/script/hassfest/quality_scale_validation/config_flow.py +++ b/script/hassfest/quality_scale_validation/config_flow.py @@ -3,10 +3,12 @@ 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.""" if not integration.config_flow: diff --git a/script/hassfest/quality_scale_validation/diagnostics.py b/script/hassfest/quality_scale_validation/diagnostics.py index 44012208bcb..ea143002b09 100644 --- a/script/hassfest/quality_scale_validation/diagnostics.py +++ b/script/hassfest/quality_scale_validation/diagnostics.py @@ -6,7 +6,7 @@ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/d import ast from script.hassfest import ast_parse_module -from script.hassfest.model import Integration +from script.hassfest.model import Config, Integration DIAGNOSTICS_FUNCTIONS = { "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.""" diagnostics_file = integration.path / "diagnostics.py" diff --git a/script/hassfest/quality_scale_validation/discovery.py b/script/hassfest/quality_scale_validation/discovery.py index db50cdba55a..d11bcaf2cec 100644 --- a/script/hassfest/quality_scale_validation/discovery.py +++ b/script/hassfest/quality_scale_validation/discovery.py @@ -6,7 +6,7 @@ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/d import ast from script.hassfest import ast_parse_module -from script.hassfest.model import Integration +from script.hassfest.model import Config, Integration MANIFEST_KEYS = [ "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.""" config_flow_file = integration.path / "config_flow.py" diff --git a/script/hassfest/quality_scale_validation/parallel_updates.py b/script/hassfest/quality_scale_validation/parallel_updates.py index 3483a44f504..00ad891774d 100644 --- a/script/hassfest/quality_scale_validation/parallel_updates.py +++ b/script/hassfest/quality_scale_validation/parallel_updates.py @@ -7,7 +7,7 @@ import ast from homeassistant.const import Platform 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: @@ -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.""" errors = [] diff --git a/script/hassfest/quality_scale_validation/reauthentication_flow.py b/script/hassfest/quality_scale_validation/reauthentication_flow.py index 81d34ec4f7f..3db9700af98 100644 --- a/script/hassfest/quality_scale_validation/reauthentication_flow.py +++ b/script/hassfest/quality_scale_validation/reauthentication_flow.py @@ -6,7 +6,7 @@ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/r import ast 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: @@ -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.""" config_flow_file = integration.path / "config_flow.py" diff --git a/script/hassfest/quality_scale_validation/reconfiguration_flow.py b/script/hassfest/quality_scale_validation/reconfiguration_flow.py index b27475e8c70..28cc0ef6d43 100644 --- a/script/hassfest/quality_scale_validation/reconfiguration_flow.py +++ b/script/hassfest/quality_scale_validation/reconfiguration_flow.py @@ -6,7 +6,7 @@ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/r import ast 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: @@ -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.""" config_flow_file = integration.path / "config_flow.py" diff --git a/script/hassfest/quality_scale_validation/runtime_data.py b/script/hassfest/quality_scale_validation/runtime_data.py index 8ad721a218c..cfc4c5224de 100644 --- a/script/hassfest/quality_scale_validation/runtime_data.py +++ b/script/hassfest/quality_scale_validation/runtime_data.py @@ -8,7 +8,7 @@ import re from homeassistant.const import Platform 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$") _FUNCTIONS: dict[str, dict[str, int]] = { @@ -102,7 +102,9 @@ def _check_typed_config_entry(integration: Integration) -> list[str]: 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.""" init_file = integration.path / "__init__.py" init = ast_parse_module(init_file) diff --git a/script/hassfest/quality_scale_validation/strict_typing.py b/script/hassfest/quality_scale_validation/strict_typing.py index a7755b6bb40..a27ab752cf0 100644 --- a/script/hassfest/quality_scale_validation/strict_typing.py +++ b/script/hassfest/quality_scale_validation/strict_typing.py @@ -7,27 +7,30 @@ from functools import lru_cache from pathlib import Path import re -from script.hassfest.model import Integration +from script.hassfest.model import Config, Integration _STRICT_TYPING_FILE = Path(".strict-typing") _COMPONENT_REGEX = r"homeassistant.components.([^.]+).*" @lru_cache -def _strict_typing_components() -> set[str]: +def _strict_typing_components(strict_typing_file: Path) -> set[str]: return set( { 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 } ) -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.""" + 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 [ "Integration does not have strict typing enabled " "(is missing from .strict-typing)" diff --git a/script/hassfest/quality_scale_validation/unique_config_entry.py b/script/hassfest/quality_scale_validation/unique_config_entry.py index 8c38923e584..83b3d20bd80 100644 --- a/script/hassfest/quality_scale_validation/unique_config_entry.py +++ b/script/hassfest/quality_scale_validation/unique_config_entry.py @@ -6,7 +6,7 @@ https://developers.home-assistant.io/docs/core/integration-quality-scale/rules/u import ast 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: @@ -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.""" if integration.manifest.get("single_config_entry"): diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py index e70bee104c9..b9259596c65 100644 --- a/tests/hassfest/test_requirements.py +++ b/tests/hassfest/test_requirements.py @@ -12,13 +12,12 @@ from script.hassfest.requirements import validate_requirements_format def integration(): """Fixture for hassfest integration model.""" return Integration( - path=Path("homeassistant/components/test"), + path=Path("homeassistant/components/test").absolute(), _config=Config( root=Path(".").absolute(), specific_integrations=None, action="validate", requirements=True, - core_integrations_path=Path("homeassistant/components"), ), _manifest={ "domain": "test", diff --git a/tests/hassfest/test_version.py b/tests/hassfest/test_version.py index 30677356101..20c3d93bda5 100644 --- a/tests/hassfest/test_version.py +++ b/tests/hassfest/test_version.py @@ -16,13 +16,12 @@ from script.hassfest.model import Config, Integration def integration(): """Fixture for hassfest integration model.""" integration = Integration( - "", + Path(), _config=Config( root=Path(".").absolute(), specific_integrations=None, action="validate", requirements=True, - core_integrations_path=Path("homeassistant/components"), ), ) integration._manifest = {