Add check for packages restricting Python version (#145690)

* Add check for packages restricting Python version

* Apply suggestions from code review

* until

* until
pull/145695/head
epenet 2025-05-27 10:44:00 +02:00 committed by GitHub
parent 7b1dfc35d1
commit 96c9636086
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 74 additions and 21 deletions

View File

@ -222,6 +222,15 @@ class Integration:
"""Add a warning."""
self.warnings.append(Error(*args, **kwargs))
def add_warning_or_error(
self, warning_only: bool, *args: Any, **kwargs: Any
) -> None:
"""Add an error or a warning."""
if warning_only:
self.add_warning(*args, **kwargs)
else:
self.add_error(*args, **kwargs)
def load_manifest(self) -> None:
"""Load manifest."""
manifest_path = self.path / "manifest.json"

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from collections import deque
from functools import cache
from importlib.metadata import metadata
import json
import os
import re
@ -319,6 +320,33 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
},
}
PYTHON_VERSION_CHECK_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
# In the form dict("domain": {"package": {"dependency1", "dependency2"}})
# - domain is the integration domain
# - package is the package (can be transitive) referencing the dependency
# - dependencyX should be the name of the referenced dependency
"bluetooth": {
# https://github.com/hbldh/bleak/pull/1718 (not yet released)
"homeassistant": {"bleak"}
},
"eq3btsmart": {
# https://github.com/EuleMitKeule/eq3btsmart/releases/tag/2.0.0
"homeassistant": {"eq3btsmart"}
},
"homekit_controller": {
# https://github.com/Jc2k/aiohomekit/issues/456
"homeassistant": {"aiohomekit"}
},
"netatmo": {
# https://github.com/jabesq-org/pyatmo/pull/533 (not yet released)
"homeassistant": {"pyatmo"}
},
"python_script": {
# Security audits are needed for each Python version
"homeassistant": {"restrictedpython"}
},
}
def validate(integrations: dict[str, Integration], config: Config) -> None:
"""Handle requirements for integrations."""
@ -489,6 +517,11 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]:
)
needs_package_version_check_exception = False
python_version_check_exceptions = PYTHON_VERSION_CHECK_EXCEPTIONS.get(
integration.domain, {}
)
needs_python_version_check_exception = False
while to_check:
package = to_check.popleft()
@ -507,22 +540,32 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]:
)
continue
if (
package in packages # Top-level checks only until bleak is resolved
and (requires_python := metadata(package)["Requires-Python"])
and not all(
_is_dependency_version_range_valid(version_part, "SemVer")
for version_part in requires_python.split(",")
)
):
needs_python_version_check_exception = True
integration.add_warning_or_error(
package in python_version_check_exceptions.get("homeassistant", set()),
"requirements",
f"Version restrictions for Python are too strict ({requires_python}) in {package}",
)
dependencies: dict[str, str] = item["dependencies"]
package_exceptions = forbidden_package_exceptions.get(package, set())
for pkg, version in dependencies.items():
if pkg.startswith("types-") or pkg in FORBIDDEN_PACKAGES:
reason = FORBIDDEN_PACKAGES.get(pkg, "not be a runtime dependency")
needs_forbidden_package_exceptions = True
if pkg in package_exceptions:
integration.add_warning(
"requirements",
f"Package {pkg} should {reason} in {package}",
)
else:
integration.add_error(
"requirements",
f"Package {pkg} should {reason} in {package}",
)
integration.add_warning_or_error(
pkg in package_exceptions,
"requirements",
f"Package {pkg} should {reason} in {package}",
)
if not check_dependency_version_range(
integration,
package,
@ -546,6 +589,12 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]:
f"Integration {integration.domain} version restrictions checks have been "
"resolved, please remove from `PACKAGE_CHECK_VERSION_RANGE_EXCEPTIONS`",
)
if python_version_check_exceptions and not needs_python_version_check_exception:
integration.add_error(
"requirements",
f"Integration {integration.domain} version restrictions for Python have "
"been resolved, please remove from `PYTHON_VERSION_CHECK_EXCEPTIONS`",
)
return all_requirements
@ -571,21 +620,16 @@ def check_dependency_version_range(
):
return True
if pkg in package_exceptions:
integration.add_warning(
"requirements",
f"Version restrictions for {pkg} are too strict ({version}) in {source}",
)
else:
integration.add_error(
"requirements",
f"Version restrictions for {pkg} are too strict ({version}) in {source}",
)
integration.add_warning_or_error(
pkg in package_exceptions,
"requirements",
f"Version restrictions for {pkg} are too strict ({version}) in {source}",
)
return False
def _is_dependency_version_range_valid(version_part: str, convention: str) -> bool:
version_match = PIP_VERSION_RANGE_SEPARATOR.match(version_part)
version_match = PIP_VERSION_RANGE_SEPARATOR.match(version_part.strip())
operator = version_match.group(1)
version = version_match.group(2)