Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore support for packages being installed from urls with fragments #109267

Merged
merged 9 commits into from
Feb 1, 2024
23 changes: 21 additions & 2 deletions homeassistant/util/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pathlib import Path
from subprocess import PIPE, Popen
import sys
from urllib.parse import urlparse

from packaging.requirements import InvalidRequirement, Requirement

Expand Down Expand Up @@ -40,14 +41,32 @@ def is_installed(requirement_str: str) -> bool:
expected input is a pip compatible package specifier (requirement string)
e.g. "package==1.0.0" or "package>=1.0.0,<2.0.0"

For backward compatibility, it also accepts a URL with a fragment
e.g. "git+https://github.com/pypa/pip#pip>=1"

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:
_LOGGER.error("Invalid requirement '%s'", requirement_str)
return False
if "#" not in requirement_str:
bdraco marked this conversation as resolved.
Show resolved Hide resolved
_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

try:
if (installed_version := version(req.name)) is None:
Expand Down
7 changes: 5 additions & 2 deletions tests/util/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
os.path.join(RESOURCE_DIR, "pyhelloworld3.zip"), TEST_NEW_REQ
)

TEST_GIT_REQ = "git+https://github.com/pypa/pip#pip>=1"


@pytest.fixture
def mock_sys():
Expand Down Expand Up @@ -230,9 +232,10 @@ def test_check_package_global() -> None:
assert not package.is_installed(f"{installed_package}<{installed_version}")


def test_check_package_zip() -> None:
"""Test for an installed zip package."""
def test_check_package_fragment() -> None:
"""Test for an installed package with a fragment."""
assert not package.is_installed(TEST_ZIP_REQ)
assert package.is_installed(TEST_GIT_REQ)


def test_get_is_installed() -> None:
Expand Down
Loading