Validate requirements format in hassfest (#55094)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
This commit is contained in:
parent
cac486440f
commit
c8f584f4ef
5 changed files with 123 additions and 9 deletions
|
@ -2,7 +2,7 @@
|
||||||
"domain": "gc100",
|
"domain": "gc100",
|
||||||
"name": "Global Cach\u00e9 GC-100",
|
"name": "Global Cach\u00e9 GC-100",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/gc100",
|
"documentation": "https://www.home-assistant.io/integrations/gc100",
|
||||||
"requirements": ["python-gc100==1.0.3a"],
|
"requirements": ["python-gc100==1.0.3a0"],
|
||||||
"codeowners": [],
|
"codeowners": [],
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1853,7 +1853,7 @@ python-forecastio==1.4.0
|
||||||
# python-gammu==3.1
|
# python-gammu==3.1
|
||||||
|
|
||||||
# homeassistant.components.gc100
|
# homeassistant.components.gc100
|
||||||
python-gc100==1.0.3a
|
python-gc100==1.0.3a0
|
||||||
|
|
||||||
# homeassistant.components.gitlab_ci
|
# homeassistant.components.gitlab_ci
|
||||||
python-gitlab==1.6.0
|
python-gitlab==1.6.0
|
||||||
|
|
|
@ -24,18 +24,19 @@ from . import (
|
||||||
from .model import Config, Integration
|
from .model import Config, Integration
|
||||||
|
|
||||||
INTEGRATION_PLUGINS = [
|
INTEGRATION_PLUGINS = [
|
||||||
json,
|
|
||||||
codeowners,
|
codeowners,
|
||||||
config_flow,
|
config_flow,
|
||||||
dependencies,
|
dependencies,
|
||||||
|
dhcp,
|
||||||
|
json,
|
||||||
manifest,
|
manifest,
|
||||||
mqtt,
|
mqtt,
|
||||||
|
requirements,
|
||||||
services,
|
services,
|
||||||
ssdp,
|
ssdp,
|
||||||
translations,
|
translations,
|
||||||
zeroconf,
|
|
||||||
dhcp,
|
|
||||||
usb,
|
usb,
|
||||||
|
zeroconf,
|
||||||
]
|
]
|
||||||
HASS_PLUGINS = [
|
HASS_PLUGINS = [
|
||||||
coverage,
|
coverage,
|
||||||
|
@ -103,9 +104,6 @@ def main():
|
||||||
|
|
||||||
plugins = [*INTEGRATION_PLUGINS]
|
plugins = [*INTEGRATION_PLUGINS]
|
||||||
|
|
||||||
if config.requirements:
|
|
||||||
plugins.append(requirements)
|
|
||||||
|
|
||||||
if config.specific_integrations:
|
if config.specific_integrations:
|
||||||
integrations = {}
|
integrations = {}
|
||||||
|
|
||||||
|
@ -122,7 +120,11 @@ def main():
|
||||||
try:
|
try:
|
||||||
start = monotonic()
|
start = monotonic()
|
||||||
print(f"Validating {plugin.__name__.split('.')[-1]}...", end="", flush=True)
|
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()
|
print()
|
||||||
plugin.validate(integrations, config)
|
plugin.validate(integrations, config)
|
||||||
print(f" done in {monotonic() - start:.2f}s")
|
print(f" done in {monotonic() - start:.2f}s")
|
||||||
|
|
|
@ -9,6 +9,7 @@ import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
|
||||||
from stdlib_list import stdlib_list
|
from stdlib_list import stdlib_list
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
@ -61,6 +62,12 @@ def normalize_package_name(requirement: str) -> str:
|
||||||
|
|
||||||
def validate(integrations: dict[str, Integration], config: Config):
|
def validate(integrations: dict[str, Integration], config: Config):
|
||||||
"""Handle requirements for integrations."""
|
"""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()
|
ensure_cache()
|
||||||
|
|
||||||
# check for incompatible requirements
|
# check for incompatible requirements
|
||||||
|
@ -74,8 +81,45 @@ def validate(integrations: dict[str, Integration], config: Config):
|
||||||
validate_requirements(integration)
|
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):
|
def validate_requirements(integration: Integration):
|
||||||
"""Validate requirements."""
|
"""Validate requirements."""
|
||||||
|
if not validate_requirements_format(integration):
|
||||||
|
return
|
||||||
|
|
||||||
# Some integrations have not been fixed yet so are allowed to have violations.
|
# Some integrations have not been fixed yet so are allowed to have violations.
|
||||||
if integration.domain in IGNORE_VIOLATIONS:
|
if integration.domain in IGNORE_VIOLATIONS:
|
||||||
return
|
return
|
||||||
|
|
68
tests/hassfest/test_requirements.py
Normal file
68
tests/hassfest/test_requirements.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue