mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Fix uv installing in user site packages (#125808)
This commit is contained in:
parent
f80cc1a247
commit
d9812f0d48
@ -8,6 +8,7 @@ from importlib.metadata import PackageNotFoundError, version
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import site
|
||||||
from subprocess import PIPE, Popen
|
from subprocess import PIPE, Popen
|
||||||
import sys
|
import sys
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@ -83,6 +84,12 @@ def is_installed(requirement_str: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
_UV_ENV_PYTHON_VARS = (
|
||||||
|
"UV_SYSTEM_PYTHON",
|
||||||
|
"UV_PYTHON",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def install_package(
|
def install_package(
|
||||||
package: str,
|
package: str,
|
||||||
upgrade: bool = True,
|
upgrade: bool = True,
|
||||||
@ -96,7 +103,18 @@ def install_package(
|
|||||||
"""
|
"""
|
||||||
_LOGGER.info("Attempting install of %s", package)
|
_LOGGER.info("Attempting install of %s", package)
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
args = ["uv", "pip", "install", "--quiet", package]
|
args = [
|
||||||
|
"uv",
|
||||||
|
"pip",
|
||||||
|
"install",
|
||||||
|
"--quiet",
|
||||||
|
package,
|
||||||
|
# We need to use unsafe-first-match for custom components
|
||||||
|
# which can use a different version of a package than the one
|
||||||
|
# we have built the wheel for.
|
||||||
|
"--index-strategy",
|
||||||
|
"unsafe-first-match",
|
||||||
|
]
|
||||||
if timeout:
|
if timeout:
|
||||||
env["HTTP_TIMEOUT"] = str(timeout)
|
env["HTTP_TIMEOUT"] = str(timeout)
|
||||||
if upgrade:
|
if upgrade:
|
||||||
@ -104,10 +122,20 @@ def install_package(
|
|||||||
if constraints is not None:
|
if constraints is not None:
|
||||||
args += ["--constraint", constraints]
|
args += ["--constraint", constraints]
|
||||||
if target:
|
if target:
|
||||||
assert not is_virtual_env()
|
abs_target = os.path.abspath(target)
|
||||||
# This only works if not running in venv
|
args += ["--target", abs_target]
|
||||||
args += ["--user"]
|
elif (
|
||||||
env["PYTHONUSERBASE"] = os.path.abspath(target)
|
not is_virtual_env()
|
||||||
|
and not (any(var in env for var in _UV_ENV_PYTHON_VARS))
|
||||||
|
and (abs_target := site.getusersitepackages())
|
||||||
|
):
|
||||||
|
# Pip compatibility
|
||||||
|
# Uv has currently no support for --user
|
||||||
|
# See https://github.com/astral-sh/uv/issues/2077
|
||||||
|
# Using workaround to install to site-packages
|
||||||
|
# https://github.com/astral-sh/uv/issues/2077#issuecomment-2150406001
|
||||||
|
args += ["--python", sys.executable, "--target", abs_target]
|
||||||
|
|
||||||
_LOGGER.debug("Running uv pip command: args=%s", args)
|
_LOGGER.debug("Running uv pip command: args=%s", args)
|
||||||
with Popen(
|
with Popen(
|
||||||
args,
|
args,
|
||||||
|
@ -93,7 +93,15 @@ def test_install(mock_popen: MagicMock, mock_env_copy: MagicMock) -> None:
|
|||||||
assert package.install_package(TEST_NEW_REQ, False)
|
assert package.install_package(TEST_NEW_REQ, False)
|
||||||
assert mock_popen.call_count == 2
|
assert mock_popen.call_count == 2
|
||||||
assert mock_popen.mock_calls[0] == call(
|
assert mock_popen.mock_calls[0] == call(
|
||||||
["uv", "pip", "install", "--quiet", TEST_NEW_REQ],
|
[
|
||||||
|
"uv",
|
||||||
|
"pip",
|
||||||
|
"install",
|
||||||
|
"--quiet",
|
||||||
|
TEST_NEW_REQ,
|
||||||
|
"--index-strategy",
|
||||||
|
"unsafe-first-match",
|
||||||
|
],
|
||||||
stdin=PIPE,
|
stdin=PIPE,
|
||||||
stdout=PIPE,
|
stdout=PIPE,
|
||||||
stderr=PIPE,
|
stderr=PIPE,
|
||||||
@ -111,7 +119,15 @@ def test_install_with_timeout(mock_popen: MagicMock, mock_env_copy: MagicMock) -
|
|||||||
assert mock_popen.call_count == 2
|
assert mock_popen.call_count == 2
|
||||||
env["HTTP_TIMEOUT"] = "10"
|
env["HTTP_TIMEOUT"] = "10"
|
||||||
assert mock_popen.mock_calls[0] == call(
|
assert mock_popen.mock_calls[0] == call(
|
||||||
["uv", "pip", "install", "--quiet", TEST_NEW_REQ],
|
[
|
||||||
|
"uv",
|
||||||
|
"pip",
|
||||||
|
"install",
|
||||||
|
"--quiet",
|
||||||
|
TEST_NEW_REQ,
|
||||||
|
"--index-strategy",
|
||||||
|
"unsafe-first-match",
|
||||||
|
],
|
||||||
stdin=PIPE,
|
stdin=PIPE,
|
||||||
stdout=PIPE,
|
stdout=PIPE,
|
||||||
stderr=PIPE,
|
stderr=PIPE,
|
||||||
@ -134,6 +150,8 @@ def test_install_upgrade(mock_popen, mock_env_copy) -> None:
|
|||||||
"install",
|
"install",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
TEST_NEW_REQ,
|
TEST_NEW_REQ,
|
||||||
|
"--index-strategy",
|
||||||
|
"unsafe-first-match",
|
||||||
"--upgrade",
|
"--upgrade",
|
||||||
],
|
],
|
||||||
stdin=PIPE,
|
stdin=PIPE,
|
||||||
@ -145,12 +163,26 @@ def test_install_upgrade(mock_popen, mock_env_copy) -> None:
|
|||||||
assert mock_popen.return_value.communicate.call_count == 1
|
assert mock_popen.return_value.communicate.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_install_target(mock_sys, mock_popen, mock_env_copy, mock_venv) -> None:
|
@pytest.mark.parametrize(
|
||||||
|
"is_venv",
|
||||||
|
[
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_install_target(
|
||||||
|
mock_sys: MagicMock,
|
||||||
|
mock_popen: MagicMock,
|
||||||
|
mock_env_copy: MagicMock,
|
||||||
|
mock_venv: MagicMock,
|
||||||
|
is_venv: bool,
|
||||||
|
) -> None:
|
||||||
"""Test an install with a target."""
|
"""Test an install with a target."""
|
||||||
target = "target_folder"
|
target = "target_folder"
|
||||||
env = mock_env_copy()
|
env = mock_env_copy()
|
||||||
env["PYTHONUSERBASE"] = os.path.abspath(target)
|
abs_target = os.path.abspath(target)
|
||||||
mock_venv.return_value = False
|
env["PYTHONUSERBASE"] = abs_target
|
||||||
|
mock_venv.return_value = is_venv
|
||||||
mock_sys.platform = "linux"
|
mock_sys.platform = "linux"
|
||||||
args = [
|
args = [
|
||||||
"uv",
|
"uv",
|
||||||
@ -158,7 +190,10 @@ def test_install_target(mock_sys, mock_popen, mock_env_copy, mock_venv) -> None:
|
|||||||
"install",
|
"install",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
TEST_NEW_REQ,
|
TEST_NEW_REQ,
|
||||||
"--user",
|
"--index-strategy",
|
||||||
|
"unsafe-first-match",
|
||||||
|
"--target",
|
||||||
|
abs_target,
|
||||||
]
|
]
|
||||||
|
|
||||||
assert package.install_package(TEST_NEW_REQ, False, target=target)
|
assert package.install_package(TEST_NEW_REQ, False, target=target)
|
||||||
@ -169,12 +204,83 @@ def test_install_target(mock_sys, mock_popen, mock_env_copy, mock_venv) -> None:
|
|||||||
assert mock_popen.return_value.communicate.call_count == 1
|
assert mock_popen.return_value.communicate.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("mock_sys", "mock_popen", "mock_env_copy", "mock_venv")
|
@pytest.mark.parametrize(
|
||||||
def test_install_target_venv() -> None:
|
("in_venv", "additional_env_vars"),
|
||||||
"""Test an install with a target in a virtual environment."""
|
[
|
||||||
target = "target_folder"
|
(True, {}),
|
||||||
with pytest.raises(AssertionError):
|
(False, {"UV_SYSTEM_PYTHON": "true"}),
|
||||||
package.install_package(TEST_NEW_REQ, False, target=target)
|
(False, {"UV_PYTHON": "python3"}),
|
||||||
|
(False, {"UV_SYSTEM_PYTHON": "true", "UV_PYTHON": "python3"}),
|
||||||
|
],
|
||||||
|
ids=["in_venv", "UV_SYSTEM_PYTHON", "UV_PYTHON", "UV_SYSTEM_PYTHON and UV_PYTHON"],
|
||||||
|
)
|
||||||
|
def test_install_pip_compatibility_no_workaround(
|
||||||
|
mock_sys: MagicMock,
|
||||||
|
mock_popen: MagicMock,
|
||||||
|
mock_env_copy: MagicMock,
|
||||||
|
mock_venv: MagicMock,
|
||||||
|
in_venv: bool,
|
||||||
|
additional_env_vars: dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
"""Test install will not use pip fallback."""
|
||||||
|
env = mock_env_copy()
|
||||||
|
env.update(additional_env_vars)
|
||||||
|
mock_venv.return_value = in_venv
|
||||||
|
mock_sys.platform = "linux"
|
||||||
|
args = [
|
||||||
|
"uv",
|
||||||
|
"pip",
|
||||||
|
"install",
|
||||||
|
"--quiet",
|
||||||
|
TEST_NEW_REQ,
|
||||||
|
"--index-strategy",
|
||||||
|
"unsafe-first-match",
|
||||||
|
]
|
||||||
|
|
||||||
|
assert package.install_package(TEST_NEW_REQ, False)
|
||||||
|
assert mock_popen.call_count == 2
|
||||||
|
assert mock_popen.mock_calls[0] == call(
|
||||||
|
args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env, close_fds=False
|
||||||
|
)
|
||||||
|
assert mock_popen.return_value.communicate.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_pip_compatibility_use_workaround(
|
||||||
|
mock_sys: MagicMock,
|
||||||
|
mock_popen: MagicMock,
|
||||||
|
mock_env_copy: MagicMock,
|
||||||
|
mock_venv: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test install will use pip compatibility fallback."""
|
||||||
|
env = mock_env_copy()
|
||||||
|
mock_venv.return_value = False
|
||||||
|
mock_sys.platform = "linux"
|
||||||
|
python = "python3"
|
||||||
|
mock_sys.executable = python
|
||||||
|
site_dir = "/site_dir"
|
||||||
|
args = [
|
||||||
|
"uv",
|
||||||
|
"pip",
|
||||||
|
"install",
|
||||||
|
"--quiet",
|
||||||
|
TEST_NEW_REQ,
|
||||||
|
"--index-strategy",
|
||||||
|
"unsafe-first-match",
|
||||||
|
"--python",
|
||||||
|
python,
|
||||||
|
"--target",
|
||||||
|
site_dir,
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch("homeassistant.util.package.site", autospec=True) as site_mock:
|
||||||
|
site_mock.getusersitepackages.return_value = site_dir
|
||||||
|
assert package.install_package(TEST_NEW_REQ, False)
|
||||||
|
|
||||||
|
assert mock_popen.call_count == 2
|
||||||
|
assert mock_popen.mock_calls[0] == call(
|
||||||
|
args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env, close_fds=False
|
||||||
|
)
|
||||||
|
assert mock_popen.return_value.communicate.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("mock_sys", "mock_venv")
|
@pytest.mark.usefixtures("mock_sys", "mock_venv")
|
||||||
@ -202,6 +308,8 @@ def test_install_constraint(mock_popen, mock_env_copy) -> None:
|
|||||||
"install",
|
"install",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
TEST_NEW_REQ,
|
TEST_NEW_REQ,
|
||||||
|
"--index-strategy",
|
||||||
|
"unsafe-first-match",
|
||||||
"--constraint",
|
"--constraint",
|
||||||
constraints,
|
constraints,
|
||||||
],
|
],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user