Fix scaffolding generations (#138820)
parent
bc5146db3c
commit
4ed4c2cc5c
|
@ -19,6 +19,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 script.util import sort_manifest as util_sort_manifest
|
||||||
|
|
||||||
from .model import Config, Integration, ScaledQualityScaleTiers
|
from .model import Config, Integration, ScaledQualityScaleTiers
|
||||||
|
|
||||||
|
@ -376,20 +377,20 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No
|
||||||
validate_version(integration)
|
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:
|
def sort_manifest(integration: Integration, config: Config) -> bool:
|
||||||
"""Sort manifest."""
|
"""Sort manifest."""
|
||||||
keys = list(integration.manifest.keys())
|
if integration.manifest_path is None:
|
||||||
if (keys_sorted := sorted(keys, key=_sort_manifest_keys)) != keys:
|
integration.add_error(
|
||||||
manifest = {key: integration.manifest[key] for key in keys_sorted}
|
"manifest",
|
||||||
|
"Manifest path not set, unable to sort manifest keys",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if util_sort_manifest(integration.manifest):
|
||||||
if config.action == "generate":
|
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"
|
text = "have been sorted"
|
||||||
else:
|
else:
|
||||||
text = "are not sorted correctly"
|
text = "are not sorted correctly"
|
||||||
|
|
|
@ -8,6 +8,7 @@ import sys
|
||||||
from script.util import valid_integration
|
from script.util import valid_integration
|
||||||
|
|
||||||
from . import docs, error, gather_info, generate
|
from . import docs, error, gather_info, generate
|
||||||
|
from .model import Info
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
p.name for p in (Path(__file__).parent / "templates").glob("*") if p.is_dir()
|
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()
|
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:
|
def main() -> int:
|
||||||
"""Scaffold an integration."""
|
"""Scaffold an integration."""
|
||||||
if not Path("requirements_all.txt").is_file():
|
if not Path("requirements_all.txt").is_file():
|
||||||
|
@ -60,36 +95,36 @@ def main() -> int:
|
||||||
|
|
||||||
generate.generate(template, info)
|
generate.generate(template, info)
|
||||||
|
|
||||||
hassfest_args = [
|
|
||||||
"python",
|
|
||||||
"-m",
|
|
||||||
"script.hassfest",
|
|
||||||
]
|
|
||||||
|
|
||||||
# If we wanted a new integration, we've already done our work.
|
# If we wanted a new integration, we've already done our work.
|
||||||
if args.template != "integration":
|
if args.template != "integration":
|
||||||
generate.generate(args.template, info)
|
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.
|
# Always output sub commands as the output will contain useful information if a command fails.
|
||||||
print("Running hassfest to pick up new information.")
|
print("Running hassfest to pick up new information.")
|
||||||
subprocess.run(hassfest_args, check=True)
|
run_process(
|
||||||
print()
|
"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.")
|
print("Running gen_requirements_all to pick up new information.")
|
||||||
subprocess.run(["python", "-m", "script.gen_requirements_all"], check=True)
|
run_process(
|
||||||
print()
|
"gen_requirements_all",
|
||||||
|
["python", "-m", "script.gen_requirements_all"],
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
|
||||||
print("Running script/translations_develop to pick up new translation strings.")
|
print("Running translations to pick up new translation strings.")
|
||||||
subprocess.run(
|
run_process(
|
||||||
|
"translations",
|
||||||
[
|
[
|
||||||
"python",
|
"python",
|
||||||
"-m",
|
"-m",
|
||||||
|
@ -98,14 +133,13 @@ def main() -> int:
|
||||||
"--integration",
|
"--integration",
|
||||||
info.domain,
|
info.domain,
|
||||||
],
|
],
|
||||||
check=True,
|
info,
|
||||||
)
|
)
|
||||||
print()
|
|
||||||
|
|
||||||
if args.develop:
|
if args.develop:
|
||||||
print("Running tests")
|
print("Running tests")
|
||||||
print(f"$ python3 -b -m pytest -vvv tests/components/{info.domain}")
|
run_process(
|
||||||
subprocess.run(
|
"pytest",
|
||||||
[
|
[
|
||||||
"python3",
|
"python3",
|
||||||
"-b",
|
"-b",
|
||||||
|
@ -114,9 +148,8 @@ def main() -> int:
|
||||||
"-vvv",
|
"-vvv",
|
||||||
f"tests/components/{info.domain}",
|
f"tests/components/{info.domain}",
|
||||||
],
|
],
|
||||||
check=True,
|
info,
|
||||||
)
|
)
|
||||||
print()
|
|
||||||
|
|
||||||
docs.print_relevant_docs(args.template, info)
|
docs.print_relevant_docs(args.template, info)
|
||||||
|
|
||||||
|
@ -126,6 +159,8 @@ def main() -> int:
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
except subprocess.CalledProcessError as err:
|
||||||
|
sys.exit(err.returncode)
|
||||||
except error.ExitApp as err:
|
except error.ExitApp as err:
|
||||||
print()
|
print()
|
||||||
print(f"Fatal Error: {err.reason}")
|
print(f"Fatal Error: {err.reason}")
|
||||||
|
|
|
@ -4,9 +4,12 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
|
from script.util import sort_manifest
|
||||||
|
|
||||||
from .const import COMPONENT_DIR, TESTS_DIR
|
from .const import COMPONENT_DIR, TESTS_DIR
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,16 +47,19 @@ class Info:
|
||||||
"""Path to the manifest."""
|
"""Path to the manifest."""
|
||||||
return COMPONENT_DIR / self.domain / "manifest.json"
|
return COMPONENT_DIR / self.domain / "manifest.json"
|
||||||
|
|
||||||
def manifest(self) -> dict:
|
def manifest(self) -> dict[str, Any]:
|
||||||
"""Return integration manifest."""
|
"""Return integration manifest."""
|
||||||
return json.loads(self.manifest_path.read_text())
|
return json.loads(self.manifest_path.read_text())
|
||||||
|
|
||||||
def update_manifest(self, **kwargs) -> None:
|
def update_manifest(self, **kwargs) -> None:
|
||||||
"""Update the integration manifest."""
|
"""Update the integration manifest."""
|
||||||
print(f"Updating {self.domain} manifest: {kwargs}")
|
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
|
@property
|
||||||
def strings_path(self) -> Path:
|
def strings_path(self) -> Path:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""Utility functions for the scaffold script."""
|
"""Utility functions for the scaffold script."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from .const import COMPONENT_DIR
|
from .const import COMPONENT_DIR
|
||||||
|
|
||||||
|
@ -13,3 +14,23 @@ def valid_integration(integration):
|
||||||
)
|
)
|
||||||
|
|
||||||
return 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
|
||||||
|
|
Loading…
Reference in New Issue