Catch blocking version pinning in dependencies early (#145364)

* Catch upper bindings in dependencies early

* One more

* Apply suggestions from code review
This commit is contained in:
epenet 2025-05-22 07:56:18 +02:00 committed by GitHub
parent f36ee88a87
commit 9e7ae1daa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -22,6 +22,19 @@ from script.gen_requirements_all import (
from .model import Config, Integration
PACKAGE_CHECK_VERSION_RANGE = {
"aiohttp": "SemVer",
# https://github.com/iMicknl/python-overkiz-api/issues/1644
# "attrs": "CalVer"
"grpcio": "SemVer",
"mashumaro": "SemVer",
"pydantic": "SemVer",
"pyjwt": "SemVer",
"pytz": "CalVer",
"typing_extensions": "SemVer",
"yarl": "SemVer",
}
PACKAGE_REGEX = re.compile(
r"^(?:--.+\s)?([-_,\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$"
)
@ -175,7 +188,7 @@ def get_pipdeptree() -> dict[str, dict[str, Any]]:
"key": "flake8-docstrings",
"package_name": "flake8-docstrings",
"installed_version": "1.5.0"
"dependencies": {"flake8"}
"dependencies": {"flake8": ">=1.2.3, <4.5.0"}
}
}
"""
@ -191,7 +204,9 @@ def get_pipdeptree() -> dict[str, dict[str, Any]]:
):
deptree[item["package"]["key"]] = {
**item["package"],
"dependencies": {dep["key"] for dep in item["dependencies"]},
"dependencies": {
dep["key"]: dep["required_version"] for dep in item["dependencies"]
},
}
return deptree
@ -222,8 +237,8 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]:
)
continue
dependencies: set[str] = item["dependencies"]
for pkg in dependencies:
dependencies: dict[str, str] = item["dependencies"]
for pkg, version in dependencies.items():
if pkg.startswith("types-") or pkg in FORBIDDEN_PACKAGES:
if package in FORBIDDEN_PACKAGE_EXCEPTIONS:
integration.add_warning(
@ -235,12 +250,62 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]:
"requirements",
f"Package {pkg} should not be a runtime dependency in {package}",
)
check_dependency_version_range(integration, package, pkg, version)
to_check.extend(dependencies)
return all_requirements
def check_dependency_version_range(
integration: Integration, source: str, pkg: str, version: str
) -> None:
"""Check requirement version range.
We want to avoid upper version bounds that are too strict for common packages.
"""
if version == "Any" or (convention := PACKAGE_CHECK_VERSION_RANGE.get(pkg)) is None:
return
if not all(
_is_dependency_version_range_valid(version_part, convention)
for version_part in version.split(";", 1)[0].split(",")
):
integration.add_error(
"requirements",
f"Version restrictions for {pkg} are too strict ({version}) in {source}",
)
def _is_dependency_version_range_valid(version_part: str, convention: str) -> bool:
version_match = PIP_VERSION_RANGE_SEPARATOR.match(version_part)
operator = version_match.group(1)
version = version_match.group(2)
if operator in (">", ">=", "!="):
# Lower version binding and version exclusion are fine
return True
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
return awesome.section(1) == 0 and awesome.section(2) == 0
if operator == "~=":
# Compatible release operator is only allowed on major or minor version
# e.g. ~=1.2 is allowed, but ~=1.2.3 is not
return awesome.section(2) == 0
return False
def install_requirements(integration: Integration, requirements: set[str]) -> bool:
"""Install integration requirements.