Fix scaffolding generations (#138820)

pull/137689/head
Steven Hartland 2025-02-19 19:23:29 +00:00 committed by GitHub
parent bc5146db3c
commit 4ed4c2cc5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 105 additions and 42 deletions

View File

@ -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"

View File

@ -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}")

View File

@ -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:

View File

@ -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