core/script/hassfest/__main__.py

238 lines
6.4 KiB
Python
Raw Normal View History

"""Validate manifests."""
import argparse
import pathlib
import sys
from time import monotonic
from . import (
Add application credentials platform (#69148) * Initial developer credentials scaffolding - Support websocket list/add/delete - Add developer credentials protocol from yaml config - Handle OAuth credential registration and de-registration - Tests for websocket and integration based registration * Fix pydoc text * Remove translations and update owners * Update homeassistant/components/developer_credentials/__init__.py Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * Update homeassistant/components/developer_credentials/__init__.py Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * Remove _async_get_developer_credential * Rename to application credentials platform * Fix race condition and add import support * Increase code coverage (92%) * Increase test coverage 93% * Increase test coverage (94%) * Increase test coverage (97%) * Increase test covearge (98%) * Increase test coverage (99%) * Increase test coverage (100%) * Remove http router frozen comment * Remove auth domain override on import * Remove debug statement * Don't import the same client id multiple times * Add auth dependency for local oauth implementation * Revert older oauth2 changes from merge * Update homeassistant/components/application_credentials/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Move config credential import to its own fixture * Override the mock_application_credentials_integration fixture instead per test * Update application credentials * Add dictionary typing * Use f-strings as per feedback * Add additional structure needed for an MVP application credential Add additional structure needed for an MVP, including a target component Xbox * Add websocket to list supported integrations for frontend selector * Application credentials config * Import xbox credentials * Remove unnecessary async calls * Update script/hassfest/application_credentials.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update script/hassfest/application_credentials.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update script/hassfest/application_credentials.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update script/hassfest/application_credentials.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Import credentials with a fixed auth domain Resolve an issue with compatibility of exisiting config entries when importing client credentials Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-04-30 15:06:43 +00:00
application_credentials,
bluetooth,
codeowners,
config_flow,
coverage,
dependencies,
dhcp,
json,
manifest,
metadata,
mqtt,
mypy_config,
requirements,
services,
ssdp,
supported_brands,
2020-04-15 23:58:20 +00:00
translations,
usb,
zeroconf,
)
from .model import Config, Integration
2019-07-31 19:25:30 +00:00
INTEGRATION_PLUGINS = [
Add application credentials platform (#69148) * Initial developer credentials scaffolding - Support websocket list/add/delete - Add developer credentials protocol from yaml config - Handle OAuth credential registration and de-registration - Tests for websocket and integration based registration * Fix pydoc text * Remove translations and update owners * Update homeassistant/components/developer_credentials/__init__.py Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * Update homeassistant/components/developer_credentials/__init__.py Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * Remove _async_get_developer_credential * Rename to application credentials platform * Fix race condition and add import support * Increase code coverage (92%) * Increase test coverage 93% * Increase test coverage (94%) * Increase test coverage (97%) * Increase test covearge (98%) * Increase test coverage (99%) * Increase test coverage (100%) * Remove http router frozen comment * Remove auth domain override on import * Remove debug statement * Don't import the same client id multiple times * Add auth dependency for local oauth implementation * Revert older oauth2 changes from merge * Update homeassistant/components/application_credentials/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Move config credential import to its own fixture * Override the mock_application_credentials_integration fixture instead per test * Update application credentials * Add dictionary typing * Use f-strings as per feedback * Add additional structure needed for an MVP application credential Add additional structure needed for an MVP, including a target component Xbox * Add websocket to list supported integrations for frontend selector * Application credentials config * Import xbox credentials * Remove unnecessary async calls * Update script/hassfest/application_credentials.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update script/hassfest/application_credentials.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update script/hassfest/application_credentials.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update script/hassfest/application_credentials.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Import credentials with a fixed auth domain Resolve an issue with compatibility of exisiting config entries when importing client credentials Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-04-30 15:06:43 +00:00
application_credentials,
bluetooth,
codeowners,
config_flow,
dependencies,
dhcp,
json,
manifest,
mqtt,
requirements,
services,
ssdp,
supported_brands,
2020-04-15 23:58:20 +00:00
translations,
usb,
zeroconf,
]
HASS_PLUGINS = [
coverage,
mypy_config,
metadata,
]
2022-01-27 04:52:09 +00:00
ALL_PLUGIN_NAMES = [
plugin.__name__.rsplit(".", maxsplit=1)[-1]
for plugin in (*INTEGRATION_PLUGINS, *HASS_PLUGINS)
]
def valid_integration_path(integration_path):
"""Test if it's a valid integration."""
path = pathlib.Path(integration_path)
if not path.is_dir():
raise argparse.ArgumentTypeError(f"{integration_path} is not a directory.")
return path
2022-01-27 04:52:09 +00:00
def validate_plugins(plugin_names: str) -> list[str]:
"""Split and validate plugin names."""
all_plugin_names = set(ALL_PLUGIN_NAMES)
plugins = plugin_names.split(",")
for plugin in plugins:
if plugin not in all_plugin_names:
raise argparse.ArgumentTypeError(f"{plugin} is not a valid plugin name")
return plugins
def get_config() -> Config:
"""Return config."""
parser = argparse.ArgumentParser(description="Hassfest")
parser.add_argument(
"--action", type=str, choices=["validate", "generate"], default=None
)
parser.add_argument(
"--integration-path",
action="append",
type=valid_integration_path,
help="Validate a single integration",
)
parser.add_argument(
"--requirements",
action="store_true",
help="Validate requirements",
)
2022-01-27 04:52:09 +00:00
parser.add_argument(
"-p",
"--plugins",
type=validate_plugins,
default=ALL_PLUGIN_NAMES,
help="Comma-separate list of plugins to run. Valid plugin names: %(default)s",
)
parsed = parser.parse_args()
if parsed.action is None:
parsed.action = "validate" if parsed.integration_path else "generate"
if parsed.action == "generate" and parsed.integration_path:
raise RuntimeError(
"Generate is not allowed when limiting to specific integrations"
)
if (
not parsed.integration_path
and not pathlib.Path("requirements_all.txt").is_file()
):
raise RuntimeError("Run from Home Assistant root")
return Config(
2019-07-31 19:25:30 +00:00
root=pathlib.Path(".").absolute(),
specific_integrations=parsed.integration_path,
action=parsed.action,
requirements=parsed.requirements,
2022-01-27 04:52:09 +00:00
plugins=set(parsed.plugins),
)
def main():
"""Validate manifests."""
try:
config = get_config()
except RuntimeError as err:
print(err)
return 1
plugins = [*INTEGRATION_PLUGINS]
if config.specific_integrations:
integrations = {}
for int_path in config.specific_integrations:
integration = Integration(int_path)
integration.load_manifest()
integrations[integration.domain] = integration
2019-04-23 06:34:37 +00:00
else:
integrations = Integration.load_dir(pathlib.Path("homeassistant/components"))
plugins += HASS_PLUGINS
for plugin in plugins:
2022-01-27 04:52:09 +00:00
plugin_name = plugin.__name__.rsplit(".", maxsplit=1)[-1]
if plugin_name not in config.plugins:
continue
try:
start = monotonic()
2022-01-27 04:52:09 +00:00
print(f"Validating {plugin_name}...", end="", flush=True)
if (
plugin is requirements
and config.requirements
and not config.specific_integrations
):
print()
plugin.validate(integrations, config)
2021-04-09 16:58:27 +00:00
print(f" done in {monotonic() - start:.2f}s")
except RuntimeError as err:
print()
print()
print("Error!")
print(err)
return 1
# When we generate, all errors that are fixable will be ignored,
# as generating them will be fixed.
2019-07-31 19:25:30 +00:00
if config.action == "generate":
general_errors = [err for err in config.errors if not err.fixable]
invalid_itg = [
2019-07-31 19:25:30 +00:00
itg
for itg in integrations.values()
if any(not error.fixable for error in itg.errors)
]
else:
# action == validate
general_errors = config.errors
invalid_itg = [itg for itg in integrations.values() if itg.errors]
warnings_itg = [itg for itg in integrations.values() if itg.warnings]
print()
print("Integrations:", len(integrations))
print("Invalid integrations:", len(invalid_itg))
print()
if not invalid_itg and not general_errors:
print_integrations_status(config, warnings_itg, show_fixable_errors=False)
if config.action == "generate":
for plugin in plugins:
2022-01-27 04:52:09 +00:00
plugin_name = plugin.__name__.rsplit(".", maxsplit=1)[-1]
if plugin_name not in config.plugins:
continue
if hasattr(plugin, "generate"):
plugin.generate(integrations, config)
return 0
2019-07-31 19:25:30 +00:00
if config.action == "generate":
print("Found errors. Generating files canceled.")
print()
if general_errors:
print("General errors:")
for error in general_errors:
print("*", error)
print()
invalid_itg.extend(itg for itg in warnings_itg if itg not in invalid_itg)
print_integrations_status(config, invalid_itg, show_fixable_errors=False)
return 1
def print_integrations_status(config, integrations, *, show_fixable_errors=True):
"""Print integration status."""
for integration in sorted(integrations, key=lambda itg: itg.domain):
extra = f" - {integration.path}" if config.specific_integrations else ""
print(f"Integration {integration.domain}{extra}:")
for error in integration.errors:
if show_fixable_errors or not error.fixable:
print("*", "[ERROR]", error)
for warning in integration.warnings:
print("*", "[WARNING]", warning)
print()
if __name__ == "__main__":
sys.exit(main())