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",
"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]]] = {
# In the form dict("domain": {"package": {"dependency1", "dependency2"}})
# - 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
"scipy": {"numpy"}
},
"noaa_tides": {
# https://github.com/GClunies/noaa_coops/pull/69
"noaa-coops": {"pandas"}
},
}
PACKAGE_REGEX = re.compile(
@ -568,7 +579,7 @@ def check_dependency_version_range(
version == "Any"
or (convention := PACKAGE_CHECK_VERSION_RANGE.get(pkg)) is None
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(",")
)
):
@ -582,22 +593,35 @@ def check_dependency_version_range(
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())
operator = version_match.group(1)
version = version_match.group(2)
awesome = AwesomeVersion(version)
if operator in (">", ">=", "!="):
# Lower version binding and version exclusion are fine
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 operator == "==":
# Explicit version with wildcard is allowed only on major version
# e.g. ==1.* is allowed, but ==1.2.* is not
return version.endswith(".*") and version.count(".") == 1
awesome = AwesomeVersion(version)
if operator in ("<", "<="):
# Upper version binding only allowed on major version
# e.g. <=3 is allowed, but <=3.1 is not

View File

@ -1,11 +1,17 @@
"""Tests for hassfest requirements."""
from pathlib import Path
from unittest.mock import patch
import pytest
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
@ -105,3 +111,41 @@ def test_validate_requirements_format_github_custom(integration: Integration) ->
integration.path = Path("")
assert validate_requirements_format(integration)
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
)