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
8 changes: 1 addition & 7 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,6 @@ jobs:
. venv/bin/activate
flake8

- run:
name: validate CODEOWNERS
command: |
. venv/bin/activate
python script/manifest/codeowners.py validate

- run:
name: run static type check
command: |
Expand All @@ -110,7 +104,7 @@ jobs:
name: validate manifests
command: |
. venv/bin/activate
python script/manifest/validate.py
python -m script.hassfest validate

- run:
name: run gen_requirements_all
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/cloud/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"hass-nabucasa==0.11"
],
"dependencies": [
"http"
"http",
"webhook"
],
"codeowners": [
"@home-assistant/core"
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/demo/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"requirements": [],
"dependencies": [
"conversation",
"zone"
"zone",
"group",
"configurator"
],
"codeowners": [
"@home-assistant/core"
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/hassio/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"documentation": "https://www.home-assistant.io/hassio",
"requirements": [],
"dependencies": [
"http"
"http",
"panel_custom"
],
"codeowners": [
"@home-assistant/hass-io"
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/map/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"name": "Map",
"documentation": "https://www.home-assistant.io/components/map",
"requirements": [],
"dependencies": [],
"dependencies": [
"frontend"
],
"codeowners": []
}
5 changes: 4 additions & 1 deletion homeassistant/components/panel_custom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,12 @@ async def async_register_panel(

async def async_setup(hass, config):
"""Initialize custom panel."""
if DOMAIN not in config:
return True

success = False

for panel in config.get(DOMAIN):
for panel in config[DOMAIN]:
name = panel[CONF_COMPONENT_NAME]

kwargs = {
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/websocket_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ def async_register_command(hass, command_or_handler, handler=None,
async def async_setup(hass, config):
"""Initialize the websocket API."""
hass.http.register_view(http.WebsocketAPIView)
commands.async_register_commands(hass)
commands.async_register_commands(hass, async_register_command)
return True
17 changes: 8 additions & 9 deletions homeassistant/components/websocket_api/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@


@callback
def async_register_commands(hass):
def async_register_commands(hass, async_reg):
"""Register commands."""
async_reg = hass.components.websocket_api.async_register_command
async_reg(handle_subscribe_events)
async_reg(handle_unsubscribe_events)
async_reg(handle_call_service)
async_reg(handle_get_states)
async_reg(handle_get_services)
async_reg(handle_get_config)
async_reg(handle_ping)
async_reg(hass, handle_subscribe_events)
async_reg(hass, handle_unsubscribe_events)
async_reg(hass, handle_call_service)
async_reg(hass, handle_get_states)
async_reg(hass, handle_get_services)
async_reg(hass, handle_get_config)
async_reg(hass, handle_ping)


def pong_message(iden):
Expand Down
27 changes: 25 additions & 2 deletions script/gen_requirements_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import fnmatch
import importlib
import os
import pathlib
import pkgutil
import re
import sys

from script.manifest.requirements import gather_requirements_from_manifests
from script.hassfest.model import Integration

COMMENT_REQUIREMENTS = (
'Adafruit-DHT',
Expand Down Expand Up @@ -219,7 +220,7 @@ def gather_modules():

errors = []

gather_requirements_from_manifests(process_requirements, errors, reqs)
gather_requirements_from_manifests(errors, reqs)
gather_requirements_from_modules(errors, reqs)

for key in reqs:
Expand All @@ -235,6 +236,28 @@ def gather_modules():
return reqs


def gather_requirements_from_manifests(errors, reqs):
"""Gather all of the requirements from manifests."""
integrations = Integration.load_dir(pathlib.Path(
'homeassistant/components'
))
for domain in sorted(integrations):
integration = integrations[domain]

if not integration.manifest:
errors.append(
'The manifest for component {} is invalid.'.format(domain)
)
continue

process_requirements(
errors,
integration.manifest['requirements'],
'homeassistant.components.{}'.format(domain),
reqs
)


def gather_requirements_from_modules(errors, reqs):
"""Collect the requirements from the modules directly."""
for package in sorted(
Expand Down
1 change: 1 addition & 0 deletions script/hassfest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Manifest validator."""
84 changes: 84 additions & 0 deletions script/hassfest/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Validate manifests."""
import pathlib
import sys

from .model import Integration, Config
from . import dependencies, manifest, codeowners

PLUGINS = [
manifest,
dependencies,
codeowners,
]


def get_config() -> Config:
"""Return config."""
if not pathlib.Path('requirements_all.txt').is_file():
raise RuntimeError("Run from project root")

return Config(
root=pathlib.Path('.').absolute(),
action='validate' if sys.argv[-1] == 'validate' else 'generate',
)


def main():
"""Validate manifests."""
try:
config = get_config()
except RuntimeError as err:
print(err)
return 1

integrations = Integration.load_dir(
pathlib.Path('homeassistant/components')
)
manifest.validate(integrations, config)
dependencies.validate(integrations, config)
codeowners.validate(integrations, config)

# When we generate, all errors that are fixable will be ignored,
# as generating them will be fixed.
if config.action == 'generate':
general_errors = [err for err in config.errors if not err.fixable]
invalid_itg = [
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]

print("Integrations:", len(integrations))
print("Invalid integrations:", len(invalid_itg))

if not invalid_itg and not general_errors:
codeowners.generate(integrations, config)
return 0

print()
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()

for integration in sorted(invalid_itg, key=lambda itg: itg.domain):
print("Integration {}:".format(integration.domain))
for error in integration.errors:
print("*", error)
print()

return 1


if __name__ == "__main__":
sys.exit(main())
85 changes: 85 additions & 0 deletions script/hassfest/codeowners.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Generate CODEOWNERS."""
from typing import Dict

from .model import Integration, Config

BASE = """
# This file is generated by script/manifest/codeowners.py
# People marked here will be automatically requested for a review
# when the code that they own is touched.
# https://github.com/blog/2392-introducing-code-owners

# Home Assistant Core
setup.py @home-assistant/core
homeassistant/*.py @home-assistant/core
homeassistant/helpers/* @home-assistant/core
homeassistant/util/* @home-assistant/core

# Virtualization
Dockerfile @home-assistant/docker
virtualization/Docker/* @home-assistant/docker

# Other code
homeassistant/scripts/check_config.py @kellerza

# Integrations
""".strip()

INDIVIDUAL_FILES = """
# Individual files
homeassistant/components/group/cover @cdce8p
homeassistant/components/demo/weather @fabaff
"""


def generate_and_validate(integrations: Dict[str, Integration]):
"""Generate CODEOWNERS."""
parts = [BASE]

for domain in sorted(integrations):
integration = integrations[domain]

if not integration.manifest:
continue

codeowners = integration.manifest['codeowners']

if not codeowners:
continue

for owner in codeowners:
if not owner.startswith('@'):
integration.add_error(
'codeowners',
'Code owners need to be valid GitHub handles.',
)

parts.append("homeassistant/components/{}/* {}".format(
domain, ' '.join(codeowners)))

parts.append('\n' + INDIVIDUAL_FILES.strip())

return '\n'.join(parts)


def validate(integrations: Dict[str, Integration], config: Config):
"""Validate CODEOWNERS."""
codeowners_path = config.root / 'CODEOWNERS'
config.cache['codeowners'] = content = generate_and_validate(integrations)

with open(str(codeowners_path), 'r') as fp:
if fp.read().strip() != content:
config.add_error(
"codeowners",
"File CODEOWNERS is not up to date. "
"Run python3 -m script.hassfest",
fixable=True
)
return


def generate(integrations: Dict[str, Integration], config: Config):
"""Generate CODEOWNERS."""
codeowners_path = config.root / 'CODEOWNERS'
with open(str(codeowners_path), 'w') as fp:
fp.write(config.cache['codeowners'] + '\n')
65 changes: 65 additions & 0 deletions script/hassfest/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Validate dependencies."""
import pathlib
import re
from typing import Set, Dict

from .model import Integration


def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) \
-> Set[str]:
"""Recursively go through a dir and it's children and find the regex."""
pattern = re.compile(search_pattern)
found = set()

for fil in path.glob(glob_pattern):
if not fil.is_file():
continue

for match in pattern.finditer(fil.read_text()):
found.add(match.groups()[0])

return found


# These components will always be set up
ALLOWED_USED_COMPONENTS = {
'persistent_notification',
}


def validate_dependencies(integration: Integration):
"""Validate all dependencies."""
# Find usage of hass.components
referenced = grep_dir(integration.path, "**/*.py",
r"hass\.components\.(\w+)")
referenced -= ALLOWED_USED_COMPONENTS
referenced -= set(integration.manifest['dependencies'])

if referenced:
for domain in sorted(referenced):
print("Warning: {} references integration {} but it's not a "
"dependency".format(integration.domain, domain))
# Not enforced yet.
# integration.add_error(
# 'dependencies',
# "Using component {} but it's not a dependency".format(domain)
# )


def validate(integrations: Dict[str, Integration], config):
"""Handle dependencies for integrations."""
# check for non-existing dependencies
for integration in integrations.values():
if not integration.manifest:
continue

validate_dependencies(integration)

# check that all referenced dependencies exist
for dep in integration.manifest['dependencies']:
if dep not in integrations:
integration.add_error(
'dependencies',
"Dependency {} does not exist"
)
Loading