From 4ed4c2cc5c887d84c2be9268e6615a03656becb6 Mon Sep 17 00:00:00 2001 From: Steven Hartland Date: Wed, 19 Feb 2025 19:23:29 +0000 Subject: [PATCH] Fix scaffolding generations (#138820) --- script/hassfest/manifest.py | 23 +++++----- script/scaffold/__main__.py | 89 ++++++++++++++++++++++++++----------- script/scaffold/model.py | 14 ++++-- script/util.py | 21 +++++++++ 4 files changed, 105 insertions(+), 42 deletions(-) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 6e9cd8bdedc..02c96930bf5 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -19,6 +19,7 @@ from voluptuous.humanize import humanize_error from homeassistant.const import Platform from homeassistant.helpers import config_validation as cv +from script.util import sort_manifest as util_sort_manifest from .model import Config, Integration, ScaledQualityScaleTiers @@ -376,20 +377,20 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No validate_version(integration) -_SORT_KEYS = {"domain": ".domain", "name": ".name"} - - -def _sort_manifest_keys(key: str) -> str: - return _SORT_KEYS.get(key, key) - - def sort_manifest(integration: Integration, config: Config) -> bool: """Sort manifest.""" - keys = list(integration.manifest.keys()) - if (keys_sorted := sorted(keys, key=_sort_manifest_keys)) != keys: - manifest = {key: integration.manifest[key] for key in keys_sorted} + if integration.manifest_path is None: + integration.add_error( + "manifest", + "Manifest path not set, unable to sort manifest keys", + ) + return False + + if util_sort_manifest(integration.manifest): if config.action == "generate": - integration.manifest_path.write_text(json.dumps(manifest, indent=2)) + integration.manifest_path.write_text( + json.dumps(integration.manifest, indent=2) + "\n" + ) text = "have been sorted" else: text = "are not sorted correctly" diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 4c102083a74..243ea9507f7 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -8,6 +8,7 @@ import sys from script.util import valid_integration from . import docs, error, gather_info, generate +from .model import Info TEMPLATES = [ p.name for p in (Path(__file__).parent / "templates").glob("*") if p.is_dir() @@ -28,6 +29,40 @@ def get_arguments() -> argparse.Namespace: return parser.parse_args() +def run_process(name: str, cmd: list[str], info: Info) -> None: + """Run a sub process and handle the result. + + :param name: The name of the sub process used in reporting. + :param cmd: The sub process arguments. + :param info: The Info object. + :raises subprocess.CalledProcessError: If the subprocess failed. + + If the sub process was successful print a success message, otherwise + print an error message and raise a subprocess.CalledProcessError. + """ + print(f"Command: {' '.join(cmd)}") + print() + result: subprocess.CompletedProcess = subprocess.run(cmd, check=False) + if result.returncode == 0: + print() + print(f"Completed {name} successfully.") + print() + return + + print() + print(f"Fatal Error: {name} failed with exit code {result.returncode}") + print() + if info.is_new: + print("This is a bug, please report an issue!") + else: + print( + "This may be an existing issue with your integration,", + "if so fix and run `script.scaffold` again,", + "otherwise please report an issue.", + ) + result.check_returncode() + + def main() -> int: """Scaffold an integration.""" if not Path("requirements_all.txt").is_file(): @@ -60,36 +95,36 @@ def main() -> int: generate.generate(template, info) - hassfest_args = [ - "python", - "-m", - "script.hassfest", - ] - # If we wanted a new integration, we've already done our work. if args.template != "integration": generate.generate(args.template, info) - else: - hassfest_args.extend( - [ - "--integration-path", - info.integration_dir, - "--skip-plugins", - "quality_scale", # Skip quality scale as it will fail for newly generated integrations. - ] - ) # Always output sub commands as the output will contain useful information if a command fails. print("Running hassfest to pick up new information.") - subprocess.run(hassfest_args, check=True) - print() + run_process( + "hassfest", + [ + "python", + "-m", + "script.hassfest", + "--integration-path", + str(info.integration_dir), + "--skip-plugins", + "quality_scale", # Skip quality scale as it will fail for newly generated integrations. + ], + info, + ) print("Running gen_requirements_all to pick up new information.") - subprocess.run(["python", "-m", "script.gen_requirements_all"], check=True) - print() + run_process( + "gen_requirements_all", + ["python", "-m", "script.gen_requirements_all"], + info, + ) - print("Running script/translations_develop to pick up new translation strings.") - subprocess.run( + print("Running translations to pick up new translation strings.") + run_process( + "translations", [ "python", "-m", @@ -98,14 +133,13 @@ def main() -> int: "--integration", info.domain, ], - check=True, + info, ) - print() if args.develop: print("Running tests") - print(f"$ python3 -b -m pytest -vvv tests/components/{info.domain}") - subprocess.run( + run_process( + "pytest", [ "python3", "-b", @@ -114,9 +148,8 @@ def main() -> int: "-vvv", f"tests/components/{info.domain}", ], - check=True, + info, ) - print() docs.print_relevant_docs(args.template, info) @@ -126,6 +159,8 @@ def main() -> int: if __name__ == "__main__": try: sys.exit(main()) + except subprocess.CalledProcessError as err: + sys.exit(err.returncode) except error.ExitApp as err: print() print(f"Fatal Error: {err.reason}") diff --git a/script/scaffold/model.py b/script/scaffold/model.py index 3b5a5e50fe4..e3a7be210ab 100644 --- a/script/scaffold/model.py +++ b/script/scaffold/model.py @@ -4,9 +4,12 @@ from __future__ import annotations import json from pathlib import Path +from typing import Any import attr +from script.util import sort_manifest + from .const import COMPONENT_DIR, TESTS_DIR @@ -44,16 +47,19 @@ class Info: """Path to the manifest.""" return COMPONENT_DIR / self.domain / "manifest.json" - def manifest(self) -> dict: + def manifest(self) -> dict[str, Any]: """Return integration manifest.""" return json.loads(self.manifest_path.read_text()) def update_manifest(self, **kwargs) -> None: """Update the integration manifest.""" print(f"Updating {self.domain} manifest: {kwargs}") - self.manifest_path.write_text( - json.dumps({**self.manifest(), **kwargs}, indent=2) + "\n" - ) + + # Sort keys in manifest so we don't trigger hassfest errors. + manifest: dict[str, Any] = {**self.manifest(), **kwargs} + sort_manifest(manifest) + + self.manifest_path.write_text(json.dumps(manifest, indent=2) + "\n") @property def strings_path(self) -> Path: diff --git a/script/util.py b/script/util.py index b7c37c72102..c9fada38c80 100644 --- a/script/util.py +++ b/script/util.py @@ -1,6 +1,7 @@ """Utility functions for the scaffold script.""" import argparse +from typing import Any from .const import COMPONENT_DIR @@ -13,3 +14,23 @@ def valid_integration(integration): ) return integration + + +_MANIFEST_SORT_KEYS = {"domain": ".domain", "name": ".name"} + + +def _sort_manifest_keys(key: str) -> str: + """Sort manifest keys.""" + return _MANIFEST_SORT_KEYS.get(key, key) + + +def sort_manifest(manifest: dict[str, Any]) -> bool: + """Sort manifest.""" + keys = list(manifest) + if (keys_sorted := sorted(keys, key=_sort_manifest_keys)) != keys: + sorted_manifest = {key: manifest[key] for key in keys_sorted} + manifest.clear() + manifest.update(sorted_manifest) + return True + + return False