1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Fix Requirement parsing in RequirementsManager (#160485)

This commit is contained in:
epenet
2026-01-12 10:55:39 +01:00
committed by GitHub
parent 0da518e951
commit 6eccbfc1cf
3 changed files with 62 additions and 30 deletions

View File

@@ -9,8 +9,6 @@ import logging
import os
from typing import Any
from packaging.requirements import Requirement
from .core import HomeAssistant, callback
from .exceptions import HomeAssistantError
from .helpers import singleton
@@ -260,8 +258,13 @@ class RequirementsManager:
"""
if DEPRECATED_PACKAGES or self.hass.config.skip_pip_packages:
all_requirements = {
requirement_string: Requirement(requirement_string)
requirement_string: requirement_details
for requirement_string in requirements
if (
requirement_details := pkg_util.parse_requirement_safe(
requirement_string
)
)
}
if DEPRECATED_PACKAGES:
for requirement_string, requirement_details in all_requirements.items():
@@ -272,9 +275,12 @@ class RequirementsManager:
"" if is_built_in else "custom ",
name,
f"has requirement '{requirement_string}' which {reason}",
f"This will stop working in Home Assistant {breaks_in_ha_version}, please"
if breaks_in_ha_version
else "Please",
(
"This will stop working in Home Assistant "
f"{breaks_in_ha_version}, please"
if breaks_in_ha_version
else "Please"
),
async_suggest_report_issue(
self.hass, integration_domain=name
),

View File

@@ -44,6 +44,39 @@ def get_installed_versions(specifiers: set[str]) -> set[str]:
return {specifier for specifier in specifiers if is_installed(specifier)}
def parse_requirement_safe(requirement_str: str) -> Requirement | None:
"""Parse a requirement string into a Requirement object.
expected input is a pip compatible package specifier (requirement string)
e.g. "package==1.0.0" or "package>=1.0.0,<2.0.0" or "package@git+https://..."
For backward compatibility, it also accepts a URL with a fragment
e.g. "git+https://github.com/pypa/pip#pip>=1"
Returns None on a badly-formed requirement string.
"""
try:
return Requirement(requirement_str)
except InvalidRequirement:
if "#" not in requirement_str:
_LOGGER.error("Invalid requirement '%s'", requirement_str)
return None
# This is likely a URL with a fragment
# example: git+https://github.com/pypa/pip#pip>=1
# fragment support was originally used to install zip files, and
# we no longer do this in Home Assistant. However, custom
# components started using it to install packages from git
# urls which would make it would be a breaking change to
# remove it.
try:
return Requirement(urlparse(requirement_str).fragment)
except InvalidRequirement:
_LOGGER.error("Invalid requirement '%s'", requirement_str)
return None
def is_installed(requirement_str: str) -> bool:
"""Check if a package is installed and will be loaded when we import it.
@@ -56,26 +89,8 @@ def is_installed(requirement_str: str) -> bool:
Returns True when the requirement is met.
Returns False when the package is not installed or doesn't meet req.
"""
try:
req = Requirement(requirement_str)
except InvalidRequirement:
if "#" not in requirement_str:
_LOGGER.error("Invalid requirement '%s'", requirement_str)
return False
# This is likely a URL with a fragment
# example: git+https://github.com/pypa/pip#pip>=1
# fragment support was originally used to install zip files, and
# we no longer do this in Home Assistant. However, custom
# components started using it to install packages from git
# urls which would make it would be a breaking change to
# remove it.
try:
req = Requirement(urlparse(requirement_str).fragment)
except InvalidRequirement:
_LOGGER.error("Invalid requirement '%s'", requirement_str)
return False
if (req := parse_requirement_safe(requirement_str)) is None:
return False
try:
if (installed_version := version(req.name)) is None:

View File

@@ -661,11 +661,12 @@ async def test_discovery_requirements_dhcp(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
("requirement", "is_built_in", "deprecation_info"),
("requirement", "is_built_in", "deprecation_prefix", "deprecation_info"),
[
(
"hello",
True,
"Detected that integration",
"which is deprecated for testing. This will stop working in Home Assistant"
" 2020.12, please create a bug report at https://github.com/home-assistant/"
"core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+test_component%22",
@@ -673,6 +674,7 @@ async def test_discovery_requirements_dhcp(hass: HomeAssistant) -> None:
(
"hello>=1.0.0",
False,
"Detected that custom integration",
"which is deprecated for testing. This will stop working in Home Assistant"
" 2020.12, please create a bug report at https://github.com/home-assistant/"
"core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+test_component%22",
@@ -680,6 +682,7 @@ async def test_discovery_requirements_dhcp(hass: HomeAssistant) -> None:
(
"pyserial-asyncio",
False,
"Detected that custom integration",
"which should be replaced by pyserial-asyncio-fast. This will stop"
" working in Home Assistant 2026.7, please create a bug report at "
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+"
@@ -688,6 +691,7 @@ async def test_discovery_requirements_dhcp(hass: HomeAssistant) -> None:
(
"pyserial-asyncio>=0.6",
True,
"Detected that integration",
"which should be replaced by pyserial-asyncio-fast. This will stop"
" working in Home Assistant 2026.7, please create a bug report at "
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+"
@@ -699,6 +703,7 @@ async def test_install_deprecated_package(
hass: HomeAssistant,
requirement: str,
is_built_in: bool,
deprecation_prefix: str,
deprecation_info: str,
caplog: pytest.LogCaptureFixture,
) -> None:
@@ -710,10 +715,16 @@ async def test_install_deprecated_package(
patch("homeassistant.util.package.install_package", return_value=True),
):
await async_process_requirements(
hass, "test_component", [requirement], is_built_in
hass,
"test_component",
[
requirement,
"git+https://github.com/user/project.git@1.2.3",
],
is_built_in,
)
assert (
f"Detected that {'' if is_built_in else 'custom '}integration "
f"'test_component' has requirement '{requirement}' {deprecation_info}"
f"{deprecation_prefix} 'test_component'"
f" has requirement '{requirement}' {deprecation_info}"
) in caplog.text