Add hassfest check to help with future dependency updates (#149624)

This commit is contained in:
Marc Mueller 2025-08-04 12:03:39 +02:00 committed by GitHub
parent fe2bd8d09e
commit f350a1a1fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 72 additions and 4 deletions

View File

@ -43,6 +43,13 @@ PACKAGE_CHECK_VERSION_RANGE = {
"urllib3": "SemVer", "urllib3": "SemVer",
"yarl": "SemVer", "yarl": "SemVer",
} }
PACKAGE_CHECK_PREPARE_UPDATE: dict[str, int] = {
# In the form dict("dependencyX": n+1)
# - dependencyX should be the name of the referenced dependency
# - current major version +1
# Pandas will only fully support Python 3.14 in v3.
"pandas": 3,
}
PACKAGE_CHECK_VERSION_RANGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { PACKAGE_CHECK_VERSION_RANGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
# In the form dict("domain": {"package": {"dependency1", "dependency2"}}) # In the form dict("domain": {"package": {"dependency1", "dependency2"}})
# - domain is the integration domain # - domain is the integration domain
@ -53,6 +60,10 @@ PACKAGE_CHECK_VERSION_RANGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
# geocachingapi > reverse_geocode > scipy > numpy # geocachingapi > reverse_geocode > scipy > numpy
"scipy": {"numpy"} "scipy": {"numpy"}
}, },
"noaa_tides": {
# https://github.com/GClunies/noaa_coops/pull/69
"noaa-coops": {"pandas"}
},
} }
PACKAGE_REGEX = re.compile( PACKAGE_REGEX = re.compile(
@ -568,7 +579,7 @@ def check_dependency_version_range(
version == "Any" version == "Any"
or (convention := PACKAGE_CHECK_VERSION_RANGE.get(pkg)) is None or (convention := PACKAGE_CHECK_VERSION_RANGE.get(pkg)) is None
or all( or all(
_is_dependency_version_range_valid(version_part, convention) _is_dependency_version_range_valid(version_part, convention, pkg)
for version_part in version.split(";", 1)[0].split(",") for version_part in version.split(";", 1)[0].split(",")
) )
): ):
@ -582,22 +593,35 @@ def check_dependency_version_range(
return False return False
def _is_dependency_version_range_valid(version_part: str, convention: str) -> bool: def _is_dependency_version_range_valid(
version_part: str, convention: str, pkg: str | None = None
) -> bool:
prepare_update = PACKAGE_CHECK_PREPARE_UPDATE.get(pkg) if pkg else None
version_match = PIP_VERSION_RANGE_SEPARATOR.match(version_part.strip()) version_match = PIP_VERSION_RANGE_SEPARATOR.match(version_part.strip())
operator = version_match.group(1) operator = version_match.group(1)
version = version_match.group(2) version = version_match.group(2)
awesome = AwesomeVersion(version)
if operator in (">", ">=", "!="): if operator in (">", ">=", "!="):
# Lower version binding and version exclusion are fine # Lower version binding and version exclusion are fine
return True return True
if prepare_update is not None:
if operator in ("==", "~="):
# Only current major version allowed which prevents updates to the next one
return False
# Allow upper constraints for major version + 1
if operator == "<" and awesome.section(0) < prepare_update + 1:
return False
if operator == "<=" and awesome.section(0) < prepare_update:
return False
if convention == "SemVer": if convention == "SemVer":
if operator == "==": if operator == "==":
# Explicit version with wildcard is allowed only on major version # Explicit version with wildcard is allowed only on major version
# e.g. ==1.* is allowed, but ==1.2.* is not # e.g. ==1.* is allowed, but ==1.2.* is not
return version.endswith(".*") and version.count(".") == 1 return version.endswith(".*") and version.count(".") == 1
awesome = AwesomeVersion(version)
if operator in ("<", "<="): if operator in ("<", "<="):
# Upper version binding only allowed on major version # Upper version binding only allowed on major version
# e.g. <=3 is allowed, but <=3.1 is not # e.g. <=3 is allowed, but <=3.1 is not

View File

@ -1,11 +1,17 @@
"""Tests for hassfest requirements.""" """Tests for hassfest requirements."""
from pathlib import Path from pathlib import Path
from unittest.mock import patch
import pytest import pytest
from script.hassfest.model import Config, Integration from script.hassfest.model import Config, Integration
from script.hassfest.requirements import validate_requirements_format from script.hassfest.requirements import (
PACKAGE_CHECK_PREPARE_UPDATE,
PACKAGE_CHECK_VERSION_RANGE,
check_dependency_version_range,
validate_requirements_format,
)
@pytest.fixture @pytest.fixture
@ -105,3 +111,41 @@ def test_validate_requirements_format_github_custom(integration: Integration) ->
integration.path = Path("") integration.path = Path("")
assert validate_requirements_format(integration) assert validate_requirements_format(integration)
assert len(integration.errors) == 0 assert len(integration.errors) == 0
@pytest.mark.parametrize(
("version", "result"),
[
(">2", True),
(">=2.0", True),
(">=2.0,<4", True),
("<4", True),
("<=3.0", True),
(">=2.0,<4;python_version<'3.14'", True),
("<3", False),
("==2.*", False),
("~=2.0", False),
("<=2.100", False),
(">2,<3", False),
(">=2.0,<3", False),
(">=2.0,<3;python_version<'3.14'", False),
],
)
def test_dependency_version_range_prepare_update(
version: str, result: bool, integration: Integration
) -> None:
"""Test dependency version range check for prepare update is working correctly."""
with (
patch.dict(PACKAGE_CHECK_VERSION_RANGE, {"numpy-test": "SemVer"}, clear=True),
patch.dict(PACKAGE_CHECK_PREPARE_UPDATE, {"numpy-test": 3}, clear=True),
):
assert (
check_dependency_version_range(
integration,
"test",
pkg="numpy-test",
version=version,
package_exceptions=set(),
)
== result
)