forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Validate component usage (home-assistant#23037)
* Update manifest validator * Update circle * Update text * Typo * fix link to codeowners * Merge CODEOWNERS into hassfest * Annotate errors with fixable * Convert error to warning * Lint * Make abs path * Python 3.5... * Typo * Fix tests
- Loading branch information
Showing
19 changed files
with
415 additions
and
220 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""Manifest validator.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
) |
Oops, something went wrong.