mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 07:05:48 +01:00
Show installed packages in cloud support package (#161516)
This commit is contained in:
@@ -42,6 +42,7 @@ from homeassistant.loader import (
|
||||
async_get_loaded_integration,
|
||||
)
|
||||
from homeassistant.util.location import async_detect_location_info
|
||||
from homeassistant.util.package import async_get_installed_packages
|
||||
|
||||
from .alexa_config import entity_supported as entity_supported_by_alexa
|
||||
from .assist_pipeline import async_create_cloud_pipeline
|
||||
@@ -571,6 +572,25 @@ class DownloadSupportPackageView(HomeAssistantView):
|
||||
"</details>\n\n"
|
||||
)
|
||||
|
||||
# Add installed packages section
|
||||
try:
|
||||
installed_packages = await async_get_installed_packages()
|
||||
except Exception: # noqa: BLE001
|
||||
# Broad exception catch for robustness in support package generation
|
||||
markdown += "## Installed packages\n\n"
|
||||
markdown += "Unable to collect installed packages information\n\n"
|
||||
else:
|
||||
if installed_packages:
|
||||
markdown += "## Installed packages\n\n"
|
||||
markdown += (
|
||||
"<details><summary>Installed packages</summary>\n\n"
|
||||
"Package | Version\n"
|
||||
"--- | ---\n"
|
||||
)
|
||||
for pkg in sorted(installed_packages, key=lambda p: p["name"].lower()):
|
||||
markdown += f"{pkg['name']} | {pkg['version']}\n"
|
||||
markdown += "\n</details>\n\n"
|
||||
|
||||
log_handler = hass.data[DATA_CLOUD_LOG_HANDLER]
|
||||
logs = "\n".join(await log_handler.get_logs(hass))
|
||||
markdown += (
|
||||
|
||||
@@ -11,15 +11,24 @@ from pathlib import Path
|
||||
import site
|
||||
from subprocess import PIPE, Popen
|
||||
import sys
|
||||
from typing import TypedDict, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from packaging.requirements import InvalidRequirement, Requirement
|
||||
|
||||
from .json import JSON_DECODE_EXCEPTIONS, json_loads_array
|
||||
from .system_info import is_official_image
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InstalledPackage(TypedDict):
|
||||
"""Represent an installed package."""
|
||||
|
||||
name: str
|
||||
version: str
|
||||
|
||||
|
||||
def is_virtual_env() -> bool:
|
||||
"""Return if we run in a virtual environment."""
|
||||
# Check supports venv && virtualenv
|
||||
@@ -203,3 +212,26 @@ async def async_get_user_site(deps_dir: str) -> str:
|
||||
)
|
||||
stdout, _ = await process.communicate()
|
||||
return stdout.decode().strip()
|
||||
|
||||
|
||||
async def async_get_installed_packages() -> list[InstalledPackage]:
|
||||
"""Return a list of installed packages and versions.
|
||||
|
||||
Returns a list of InstalledPackage dicts with 'name' and 'version' keys.
|
||||
"""
|
||||
args = [sys.executable, "-m", "uv", "pip", "list", "--format", "json"]
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*args,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
close_fds=False, # required for posix_spawn
|
||||
)
|
||||
stdout, _ = await process.communicate()
|
||||
if process.returncode != 0:
|
||||
return []
|
||||
|
||||
try:
|
||||
return cast(list[InstalledPackage], json_loads_array(stdout.decode()))
|
||||
except (*JSON_DECODE_EXCEPTIONS, ValueError):
|
||||
return []
|
||||
|
||||
@@ -87,6 +87,17 @@
|
||||
|
||||
</details>
|
||||
|
||||
## Installed packages
|
||||
|
||||
<details><summary>Installed packages</summary>
|
||||
|
||||
Package | Version
|
||||
--- | ---
|
||||
hass-nabucasa | 1.2.3
|
||||
homeassistant | 3.2.1
|
||||
|
||||
</details>
|
||||
|
||||
## Full logs
|
||||
|
||||
<details><summary>Logs</summary>
|
||||
@@ -181,6 +192,17 @@
|
||||
|
||||
</details>
|
||||
|
||||
## Installed packages
|
||||
|
||||
<details><summary>Installed packages</summary>
|
||||
|
||||
Package | Version
|
||||
--- | ---
|
||||
hass-nabucasa | 1.2.3
|
||||
homeassistant | 3.2.1
|
||||
|
||||
</details>
|
||||
|
||||
## Full logs
|
||||
|
||||
<details><summary>Logs</summary>
|
||||
@@ -246,6 +268,17 @@
|
||||
|
||||
</details>
|
||||
|
||||
## Installed packages
|
||||
|
||||
<details><summary>Installed packages</summary>
|
||||
|
||||
Package | Version
|
||||
--- | ---
|
||||
hass-nabucasa | 1.2.3
|
||||
homeassistant | 3.2.1
|
||||
|
||||
</details>
|
||||
|
||||
## Full logs
|
||||
|
||||
<details><summary>Logs</summary>
|
||||
|
||||
@@ -1953,6 +1953,13 @@ async def test_download_support_package(
|
||||
"user": "hass",
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.cloud.http_api.async_get_installed_packages",
|
||||
return_value=[
|
||||
{"name": "homeassistant", "version": "3.2.1"},
|
||||
{"name": "hass-nabucasa", "version": "1.2.3"},
|
||||
],
|
||||
),
|
||||
):
|
||||
req = await cloud_client.get("/api/cloud/support_package")
|
||||
assert req.status == HTTPStatus.OK
|
||||
@@ -2065,6 +2072,13 @@ async def test_download_support_package_custom_components_error(
|
||||
"homeassistant.components.cloud.http_api.async_get_custom_components",
|
||||
side_effect=Exception("Custom components error"),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.cloud.http_api.async_get_installed_packages",
|
||||
return_value=[
|
||||
{"name": "homeassistant", "version": "3.2.1"},
|
||||
{"name": "hass-nabucasa", "version": "1.2.3"},
|
||||
],
|
||||
),
|
||||
):
|
||||
req = await cloud_client.get("/api/cloud/support_package")
|
||||
assert req.status == HTTPStatus.OK
|
||||
@@ -2182,6 +2196,13 @@ async def test_download_support_package_integration_load_error(
|
||||
if domain == "failing_integration"
|
||||
else async_get_loaded_integration(hass, domain),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.cloud.http_api.async_get_installed_packages",
|
||||
return_value=[
|
||||
{"name": "homeassistant", "version": "3.2.1"},
|
||||
{"name": "hass-nabucasa", "version": "1.2.3"},
|
||||
],
|
||||
),
|
||||
):
|
||||
req = await cloud_client.get("/api/cloud/support_package")
|
||||
assert req.status == HTTPStatus.OK
|
||||
|
||||
@@ -361,6 +361,69 @@ async def test_async_get_user_site(mock_env_copy) -> None:
|
||||
assert ret == os.path.join(deps_dir, "lib_dir")
|
||||
|
||||
|
||||
async def test_async_get_installed_packages() -> None:
|
||||
"""Test async get installed packages."""
|
||||
mock_output = b'[{"name": "package1", "version": "1.0.0"}, {"name": "package2", "version": "2.0.0"}]'
|
||||
|
||||
async_popen = MagicMock()
|
||||
async_popen.returncode = 0
|
||||
|
||||
async def communicate(input=None):
|
||||
return (mock_output, None)
|
||||
|
||||
async_popen.communicate = communicate
|
||||
|
||||
args = [sys.executable, "-m", "uv", "pip", "list", "--format", "json"]
|
||||
with patch(
|
||||
"homeassistant.util.package.asyncio.create_subprocess_exec",
|
||||
return_value=async_popen,
|
||||
) as popen_mock:
|
||||
ret = await package.async_get_installed_packages()
|
||||
|
||||
assert popen_mock.call_count == 1
|
||||
assert popen_mock.call_args == call(
|
||||
*args,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
close_fds=False,
|
||||
)
|
||||
assert ret == [
|
||||
{"name": "package1", "version": "1.0.0"},
|
||||
{"name": "package2", "version": "2.0.0"},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("returncode", "stdout", "test_id"),
|
||||
[
|
||||
(1, b"", "nonzero_return"),
|
||||
(0, b'{"name": "package1", "version": "1.0.0"}', "json_object"),
|
||||
(0, b'"just a string"', "json_string"),
|
||||
],
|
||||
ids=lambda x: x if isinstance(x, str) else None,
|
||||
)
|
||||
async def test_async_get_installed_packages_returns_empty(
|
||||
returncode: int, stdout: bytes, test_id: str
|
||||
) -> None:
|
||||
"""Test async get installed packages returns empty list on errors."""
|
||||
async_popen = MagicMock()
|
||||
async_popen.returncode = returncode
|
||||
|
||||
async def communicate(input=None):
|
||||
return (stdout, None)
|
||||
|
||||
async_popen.communicate = communicate
|
||||
|
||||
with patch(
|
||||
"homeassistant.util.package.asyncio.create_subprocess_exec",
|
||||
return_value=async_popen,
|
||||
):
|
||||
ret = await package.async_get_installed_packages()
|
||||
|
||||
assert ret == []
|
||||
|
||||
|
||||
def test_check_package_global(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Test for an installed package."""
|
||||
pkg = metadata("homeassistant")
|
||||
|
||||
Reference in New Issue
Block a user