Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 53 additions & 22 deletions script/scaffold/__main__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
"""Validate manifests."""
import argparse
from pathlib import Path
import subprocess
import sys

from . import gather_info, generate, error, model
from . import gather_info, generate, error
from .const import COMPONENT_DIR


TEMPLATES = [
p.name for p in (Path(__file__).parent / "templates").glob("*") if p.is_dir()
]


def valid_integration(integration):
"""Test if it's a valid integration."""
if not (COMPONENT_DIR / integration).exists():
raise argparse.ArgumentTypeError(
f"The integration {integration} does not exist."
)

return integration


def get_arguments() -> argparse.Namespace:
"""Get parsed passed in arguments."""
parser = argparse.ArgumentParser(description="Home Assistant Scaffolder")
parser.add_argument("template", type=str, choices=TEMPLATES)
parser.add_argument(
"--develop", action="store_true", help="Automatically fill in info"
)
parser.add_argument(
"--integration", type=valid_integration, help="Integration to target."
)

arguments = parser.parse_args()

return arguments


def main():
Expand All @@ -12,29 +45,22 @@ def main():
print("Run from project root")
return 1

print("Creating a new integration for Home Assistant.")
args = get_arguments()

if "--develop" in sys.argv:
print("Running in developer mode. Automatically filling in info.")
print()
info = gather_info.gather_info(args)

info = model.Info(
domain="develop",
name="Develop Hub",
codeowner="@developer",
requirement="aiodevelop==1.2.3",
)
else:
try:
info = gather_info.gather_info()
except error.ExitApp as err:
print()
print(err.reason)
return err.exit_code
generate.generate(args.template, info)

# If creating new integration, create config flow too
if args.template == "integration":
if info.authentication or not info.discoverable:
template = "config_flow"
else:
template = "config_flow_discovery"

generate.generate(info)
generate.generate(template, info)

print("Running hassfest to pick up new codeowner and config flow.")
print("Running hassfest to pick up new information.")
subprocess.run("python -m script.hassfest", shell=True)
print()

Expand All @@ -47,10 +73,15 @@ def main():
return 1
print()

print(f"Successfully created the {info.domain} integration!")
print(f"Done!")

return 0


if __name__ == "__main__":
sys.exit(main())
try:
sys.exit(main())
except error.ExitApp as err:
print()
print(f"Fatal Error: {err.reason}")
sys.exit(err.exit_code)
22 changes: 22 additions & 0 deletions script/scaffold/docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Print links to relevant docs."""
from .model import Info


def print_relevant_docs(template: str, info: Info) -> None:
"""Print relevant docs."""
if template == "integration":
print(
f"""
Your integration has been created at {info.integration_dir} . Next step is to fill in the blanks for the code marked with TODO.

For a breakdown of each file, check the developer documentation at:
https://developers.home-assistant.io/docs/en/creating_integration_file_structure.html
"""
)

elif template == "config_flow":
print(
f"""
The config flow has been added to the {info.domain} integration. Next step is to fill in the blanks for the code marked with TODO.
"""
)
2 changes: 1 addition & 1 deletion script/scaffold/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class ExitApp(Exception):
"""Exception to indicate app should exit."""

def __init__(self, reason, exit_code):
def __init__(self, reason, exit_code=1):
"""Initialize the exit app exception."""
self.reason = reason
self.exit_code = exit_code
188 changes: 146 additions & 42 deletions script/scaffold/gather_info.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Gather info for scaffolding."""
import json

from homeassistant.util import slugify

from .const import COMPONENT_DIR
Expand All @@ -9,49 +11,142 @@
CHECK_EMPTY = ["Cannot be empty", lambda value: value]


FIELDS = {
"domain": {
"prompt": "What is the domain?",
"validators": [
CHECK_EMPTY,
[
"Domains cannot contain spaces or special characters.",
lambda value: value == slugify(value),
],
[
"There already is an integration with this domain.",
lambda value: not (COMPONENT_DIR / value).exists(),
],
],
},
"name": {
"prompt": "What is the name of your integration?",
"validators": [CHECK_EMPTY],
},
"codeowner": {
"prompt": "What is your GitHub handle?",
"validators": [
CHECK_EMPTY,
[
'GitHub handles need to start with an "@"',
lambda value: value.startswith("@"),
],
],
},
"requirement": {
"prompt": "What PyPI package and version do you depend on? Leave blank for none.",
"validators": [
["Versions should be pinned using '=='.", lambda value: "==" in value]
],
},
}


def gather_info() -> Info:
def gather_info(arguments) -> Info:
"""Gather info."""
existing = arguments.template != "integration"

if arguments.develop:
print("Running in developer mode. Automatically filling in info.")
print()

if existing:
if arguments.develop:
return _load_existing_integration("develop")

if arguments.integration:
return _load_existing_integration(arguments.integration)

return gather_existing_integration()

if arguments.develop:
return Info(
domain="develop",
name="Develop Hub",
codeowner="@developer",
requirement="aiodevelop==1.2.3",
)

return gather_new_integration()


def gather_new_integration() -> Info:
"""Gather info about new integration from user."""
return Info(
**_gather_info(
{
"domain": {
"prompt": "What is the domain?",
"validators": [
CHECK_EMPTY,
[
"Domains cannot contain spaces or special characters.",
lambda value: value == slugify(value),
],
[
"There already is an integration with this domain.",
lambda value: not (COMPONENT_DIR / value).exists(),
],
],
},
"name": {
"prompt": "What is the name of your integration?",
"validators": [CHECK_EMPTY],
},
"codeowner": {
"prompt": "What is your GitHub handle?",
"validators": [
CHECK_EMPTY,
[
'GitHub handles need to start with an "@"',
lambda value: value.startswith("@"),
],
],
},
"requirement": {
"prompt": "What PyPI package and version do you depend on? Leave blank for none.",
"validators": [
[
"Versions should be pinned using '=='.",
lambda value: not value or "==" in value,
]
],
},
"authentication": {
"prompt": "Does Home Assistant need the user to authenticate to control the device/service? (yes/no)",
"default": "yes",
"validators": [
[
"Type either 'yes' or 'no'",
lambda value: value in ("yes", "no"),
]
],
"convertor": lambda value: value == "yes",
},
"discoverable": {
"prompt": "Is the device/service discoverable on the local network? (yes/no)",
"default": "no",
"validators": [
[
"Type either 'yes' or 'no'",
lambda value: value in ("yes", "no"),
]
],
"convertor": lambda value: value == "yes",
},
}
)
)


def gather_existing_integration() -> Info:
"""Gather info about existing integration from user."""
answers = _gather_info(
{
"domain": {
"prompt": "What is the domain?",
"validators": [
CHECK_EMPTY,
[
"Domains cannot contain spaces or special characters.",
lambda value: value == slugify(value),
],
[
"This integration does not exist.",
lambda value: (COMPONENT_DIR / value).exists(),
],
],
}
}
)

return _load_existing_integration(answers["domain"])


def _load_existing_integration(domain) -> Info:
"""Load an existing integration."""
if not (COMPONENT_DIR / domain).exists():
raise ExitApp("Integration does not exist", 1)

manifest = json.loads((COMPONENT_DIR / domain / "manifest.json").read_text())

return Info(domain=domain, name=manifest["name"])


def _gather_info(fields) -> dict:
"""Gather info from user."""
answers = {}

for key, info in FIELDS.items():
for key, info in fields.items():
hint = None
while key not in answers:
if hint is not None:
Expand All @@ -60,11 +155,18 @@ def gather_info() -> Info:

try:
print()
value = input(info["prompt"] + "\n> ")
msg = info["prompt"]
if "default" in info:
msg += f" [{info['default']}]"
value = input(f"{msg}\n> ")
except (KeyboardInterrupt, EOFError):
raise ExitApp("Interrupted!", 1)

value = value.strip()

if value == "" and "default" in info:
value = info["default"]

hint = None

for validator_hint, validator in info["validators"]:
Expand All @@ -73,7 +175,9 @@ def gather_info() -> Info:
break

if hint is None:
if "convertor" in info:
value = info["convertor"](value)
answers[key] = value

print()
return Info(**answers)
return answers
Loading