mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +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 os
|
||||
from pathlib import Path
|
||||
import site
|
||||
from subprocess import PIPE, Popen
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
@ -83,6 +84,12 @@ def is_installed(requirement_str: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
_UV_ENV_PYTHON_VARS = (
|
||||
"UV_SYSTEM_PYTHON",
|
||||
"UV_PYTHON",
|
||||
)
|
||||
|
||||
|
||||
def install_package(
|
||||
package: str,
|
||||
upgrade: bool = True,
|
||||
@ -96,7 +103,18 @@ def install_package(
|
||||
"""
|
||||
_LOGGER.info("Attempting install of %s", package)
|
||||
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:
|
||||
env["HTTP_TIMEOUT"] = str(timeout)
|
||||
if upgrade:
|
||||
@ -104,10 +122,20 @@ def install_package(
|
||||
if constraints is not None:
|
||||
args += ["--constraint", constraints]
|
||||
if target:
|
||||
assert not is_virtual_env()
|
||||
# This only works if not running in venv
|
||||
args += ["--user"]
|
||||
env["PYTHONUSERBASE"] = os.path.abspath(target)
|
||||
abs_target = os.path.abspath(target)
|
||||
args += ["--target", abs_target]
|
||||
elif (
|
||||
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)
|
||||
with Popen(
|
||||
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 mock_popen.call_count == 2
|
||||
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,
|
||||
stdout=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
|
||||
env["HTTP_TIMEOUT"] = "10"
|
||||
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,
|
||||
stdout=PIPE,
|
||||
stderr=PIPE,
|
||||
@ -134,6 +150,8 @@ def test_install_upgrade(mock_popen, mock_env_copy) -> None:
|
||||
"install",
|
||||
"--quiet",
|
||||
TEST_NEW_REQ,
|
||||
"--index-strategy",
|
||||
"unsafe-first-match",
|
||||
"--upgrade",
|
||||
],
|
||||
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
|
||||
|
||||
|
||||
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."""
|
||||
target = "target_folder"
|
||||
env = mock_env_copy()
|
||||
env["PYTHONUSERBASE"] = os.path.abspath(target)
|
||||
mock_venv.return_value = False
|
||||
abs_target = os.path.abspath(target)
|
||||
env["PYTHONUSERBASE"] = abs_target
|
||||
mock_venv.return_value = is_venv
|
||||
mock_sys.platform = "linux"
|
||||
args = [
|
||||
"uv",
|
||||
@ -158,7 +190,10 @@ def test_install_target(mock_sys, mock_popen, mock_env_copy, mock_venv) -> None:
|
||||
"install",
|
||||
"--quiet",
|
||||
TEST_NEW_REQ,
|
||||
"--user",
|
||||
"--index-strategy",
|
||||
"unsafe-first-match",
|
||||
"--target",
|
||||
abs_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
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_sys", "mock_popen", "mock_env_copy", "mock_venv")
|
||||
def test_install_target_venv() -> None:
|
||||
"""Test an install with a target in a virtual environment."""
|
||||
target = "target_folder"
|
||||
with pytest.raises(AssertionError):
|
||||
package.install_package(TEST_NEW_REQ, False, target=target)
|
||||
@pytest.mark.parametrize(
|
||||
("in_venv", "additional_env_vars"),
|
||||
[
|
||||
(True, {}),
|
||||
(False, {"UV_SYSTEM_PYTHON": "true"}),
|
||||
(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")
|
||||
@ -202,6 +308,8 @@ def test_install_constraint(mock_popen, mock_env_copy) -> None:
|
||||
"install",
|
||||
"--quiet",
|
||||
TEST_NEW_REQ,
|
||||
"--index-strategy",
|
||||
"unsafe-first-match",
|
||||
"--constraint",
|
||||
constraints,
|
||||
],
|
||||
|
Loading…
x
Reference in New Issue
Block a user