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:
@@ -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
|
||||
),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user