diff --git a/homeassistant/components/gc100/manifest.json b/homeassistant/components/gc100/manifest.json index 55ea7d94682..8caa2f91204 100644 --- a/homeassistant/components/gc100/manifest.json +++ b/homeassistant/components/gc100/manifest.json @@ -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" } diff --git a/requirements_all.txt b/requirements_all.txt index bc759d50a5b..5aafc1e7046 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 1bec328702e..d4935196cc7 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -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") diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 5927824b21f..f72562f7f2f 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -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 "==".', + ) + 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 diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py new file mode 100644 index 00000000000..c65716d5d92 --- /dev/null +++ b/tests/hassfest/test_requirements.py @@ -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 "==".' 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