Validate requirements format in hassfest (#55094)

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
This commit is contained in:
Paulus Schoutsen 2021-08-23 23:51:07 -07:00 committed by GitHub
parent cac486440f
commit c8f584f4ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 123 additions and 9 deletions

View file

@ -2,7 +2,7 @@
"domain": "gc100",
"name": "Global Cach\u00e9 GC-100",
"documentation": "https://www.home-assistant.io/integrations/gc100",
"requirements": ["python-gc100==1.0.3a"],
"requirements": ["python-gc100==1.0.3a0"],
"codeowners": [],
"iot_class": "local_polling"
}

View file

@ -1853,7 +1853,7 @@ python-forecastio==1.4.0
# python-gammu==3.1
# homeassistant.components.gc100
python-gc100==1.0.3a
python-gc100==1.0.3a0
# homeassistant.components.gitlab_ci
python-gitlab==1.6.0

View file

@ -24,18 +24,19 @@ from . import (
from .model import Config, Integration
INTEGRATION_PLUGINS = [
json,
codeowners,
config_flow,
dependencies,
dhcp,
json,
manifest,
mqtt,
requirements,
services,
ssdp,
translations,
zeroconf,
dhcp,
usb,
zeroconf,
]
HASS_PLUGINS = [
coverage,
@ -103,9 +104,6 @@ def main():
plugins = [*INTEGRATION_PLUGINS]
if config.requirements:
plugins.append(requirements)
if config.specific_integrations:
integrations = {}
@ -122,7 +120,11 @@ def main():
try:
start = monotonic()
print(f"Validating {plugin.__name__.split('.')[-1]}...", end="", flush=True)
if plugin is requirements and not config.specific_integrations:
if (
plugin is requirements
and config.requirements
and not config.specific_integrations
):
print()
plugin.validate(integrations, config)
print(f" done in {monotonic() - start:.2f}s")

View file

@ -9,6 +9,7 @@ import re
import subprocess
import sys
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
from stdlib_list import stdlib_list
from tqdm import tqdm
@ -61,6 +62,12 @@ def normalize_package_name(requirement: str) -> str:
def validate(integrations: dict[str, Integration], config: Config):
"""Handle requirements for integrations."""
# Check if we are doing format-only validation.
if not config.requirements:
for integration in integrations.values():
validate_requirements_format(integration)
return
ensure_cache()
# check for incompatible requirements
@ -74,8 +81,45 @@ def validate(integrations: dict[str, Integration], config: Config):
validate_requirements(integration)
def validate_requirements_format(integration: Integration) -> bool:
"""Validate requirements format.
Returns if valid.
"""
start_errors = len(integration.errors)
for req in integration.requirements:
if " " in req:
integration.add_error(
"requirements",
f'Requirement "{req}" contains a space',
)
continue
pkg, sep, version = req.partition("==")
if not sep and integration.core:
integration.add_error(
"requirements",
f'Requirement {req} need to be pinned "<pkg name>==<version>".',
)
continue
if AwesomeVersion(version).strategy == AwesomeVersionStrategy.UNKNOWN:
integration.add_error(
"requirements",
f"Unable to parse package version ({version}) for {pkg}.",
)
continue
return len(integration.errors) == start_errors
def validate_requirements(integration: Integration):
"""Validate requirements."""
if not validate_requirements_format(integration):
return
# Some integrations have not been fixed yet so are allowed to have violations.
if integration.domain in IGNORE_VIOLATIONS:
return

View file

@ -0,0 +1,68 @@
"""Tests for hassfest requirements."""
from pathlib import Path
import pytest
from script.hassfest.model import Integration
from script.hassfest.requirements import validate_requirements_format
@pytest.fixture
def integration():
"""Fixture for hassfest integration model."""
integration = Integration(
path=Path("homeassistant/components/test"),
manifest={
"domain": "test",
"documentation": "https://example.com",
"name": "test",
"codeowners": ["@awesome"],
"requirements": [],
},
)
yield integration
def test_validate_requirements_format_with_space(integration: Integration):
"""Test validate requirement with space around separator."""
integration.manifest["requirements"] = ["test_package == 1"]
assert not validate_requirements_format(integration)
assert len(integration.errors) == 1
assert 'Requirement "test_package == 1" contains a space' in [
x.error for x in integration.errors
]
def test_validate_requirements_format_wrongly_pinned(integration: Integration):
"""Test requirement with loose pin."""
integration.manifest["requirements"] = ["test_package>=1"]
assert not validate_requirements_format(integration)
assert len(integration.errors) == 1
assert 'Requirement test_package>=1 need to be pinned "<pkg name>==<version>".' in [
x.error for x in integration.errors
]
def test_validate_requirements_format_ignore_pin_for_custom(integration: Integration):
"""Test requirement ignore pinning for custom."""
integration.manifest["requirements"] = ["test_package>=1"]
integration.path = Path("")
assert validate_requirements_format(integration)
assert len(integration.errors) == 0
def test_validate_requirements_format_invalid_version(integration: Integration):
"""Test requirement with invalid version."""
integration.manifest["requirements"] = ["test_package==invalid"]
assert not validate_requirements_format(integration)
assert len(integration.errors) == 1
assert "Unable to parse package version (invalid) for test_package." in [
x.error for x in integration.errors
]
def test_validate_requirements_format_successful(integration: Integration):
"""Test requirement with successful result."""
integration.manifest["requirements"] = ["test_package==1.2.3"]
assert validate_requirements_format(integration)
assert len(integration.errors) == 0