Refactor builtin repositories to enum (#5976)

This commit is contained in:
Mike Degatano 2025-06-30 13:22:00 -04:00 committed by GitHub
parent d1c1a2d418
commit 38750d74a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 101 additions and 44 deletions

View File

@ -1,5 +1,6 @@
"""Init file for Supervisor add-on Git."""
from abc import ABC, abstractmethod
import asyncio
import functools as ft
import logging
@ -7,19 +8,19 @@ from pathlib import Path
import git
from ..const import ATTR_BRANCH, ATTR_URL, URL_HASSIO_ADDONS
from ..const import ATTR_BRANCH, ATTR_URL
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import StoreGitCloneError, StoreGitError, StoreJobError
from ..jobs.decorator import Job, JobCondition
from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils import remove_folder
from .utils import get_hash_from_repository
from .validate import RE_REPOSITORY
from .validate import RE_REPOSITORY, BuiltinRepository
_LOGGER: logging.Logger = logging.getLogger(__name__)
class GitRepo(CoreSysAttributes):
class GitRepo(CoreSysAttributes, ABC):
"""Manage Add-on Git repository."""
builtin: bool
@ -197,29 +198,23 @@ class GitRepo(CoreSysAttributes):
)
raise StoreGitError() from err
async def _remove(self):
@abstractmethod
async def remove(self) -> None:
"""Remove a repository."""
if self.lock.locked():
_LOGGER.warning("There is already a task in progress")
return
def _remove_git_dir(path: Path) -> None:
if not path.is_dir():
return
remove_folder(path)
async with self.lock:
await self.sys_run_in_executor(_remove_git_dir, self.path)
class GitRepoHassIO(GitRepo):
"""Supervisor add-ons repository."""
class GitRepoBuiltin(GitRepo):
"""Built-in add-ons repository."""
builtin: bool = False
builtin: bool = True
def __init__(self, coresys):
def __init__(self, coresys: CoreSys, repository: BuiltinRepository):
"""Initialize Git Supervisor add-on repository."""
super().__init__(coresys, coresys.config.path_addons_core, URL_HASSIO_ADDONS)
super().__init__(coresys, repository.get_path(coresys), repository.url)
async def remove(self) -> None:
"""Raise. Cannot remove built-in repositories."""
raise RuntimeError("Cannot remove built-in repositories!")
class GitRepoCustom(GitRepo):
@ -233,7 +228,21 @@ class GitRepoCustom(GitRepo):
super().__init__(coresys, path, url)
async def remove(self):
async def remove(self) -> None:
"""Remove a custom repository."""
if self.lock.locked():
_LOGGER.warning(
"Cannot remove add-on repository %s, there is already a task in progress",
self.url,
)
return
_LOGGER.info("Removing custom add-on repository %s", self.url)
await self._remove()
def _remove_git_dir(path: Path) -> None:
if not path.is_dir():
return
remove_folder(path)
async with self.lock:
await self.sys_run_in_executor(_remove_git_dir, self.path)

View File

@ -2,7 +2,6 @@
import logging
from pathlib import Path
from typing import cast
import voluptuous as vol
@ -13,9 +12,9 @@ from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import ConfigurationFileError, StoreError
from ..utils.common import read_json_or_yaml_file
from .const import StoreType
from .git import GitRepo, GitRepoCustom, GitRepoHassIO
from .git import GitRepo, GitRepoBuiltin, GitRepoCustom
from .utils import get_hash_from_repository
from .validate import SCHEMA_REPOSITORY_CONFIG
from .validate import SCHEMA_REPOSITORY_CONFIG, BuiltinRepository
_LOGGER: logging.Logger = logging.getLogger(__name__)
UNKNOWN = "unknown"
@ -34,10 +33,11 @@ class Repository(CoreSysAttributes):
self._slug = repository
self._type = StoreType.LOCAL
self._latest_mtime: float | None = None
elif repository == StoreType.CORE:
self.git = GitRepoHassIO(coresys)
self._slug = repository
self._type = StoreType.CORE
elif repository in BuiltinRepository:
builtin = BuiltinRepository(repository)
self.git = GitRepoBuiltin(coresys, builtin)
self._slug = builtin.id
self._type = builtin.type
else:
self.git = GitRepoCustom(coresys, repository)
self._slug = get_hash_from_repository(repository)
@ -140,7 +140,7 @@ class Repository(CoreSysAttributes):
async def remove(self) -> None:
"""Remove add-on repository."""
if not self.git or self.type == StoreType.CORE:
if not self.git or self.git.builtin:
raise StoreError("Can't remove built-in repositories!", _LOGGER.error)
await cast(GitRepoCustom, self.git).remove()
await self.git.remove()

View File

@ -1,21 +1,62 @@
"""Validate add-ons options schema."""
from enum import StrEnum
from pathlib import Path
import voluptuous as vol
from ..const import ATTR_MAINTAINER, ATTR_NAME, ATTR_REPOSITORIES, ATTR_URL
from ..const import (
ATTR_MAINTAINER,
ATTR_NAME,
ATTR_REPOSITORIES,
ATTR_URL,
URL_HASSIO_ADDONS,
)
from ..coresys import CoreSys
from ..validate import RE_REPOSITORY
from .const import StoreType
from .utils import get_hash_from_repository
URL_COMMUNITY_ADDONS = "https://github.com/hassio-addons/repository"
URL_ESPHOME = "https://github.com/esphome/home-assistant-addon"
URL_MUSIC_ASSISTANT = "https://github.com/music-assistant/home-assistant-addon"
BUILTIN_REPOSITORIES = {
StoreType.CORE,
StoreType.LOCAL,
URL_COMMUNITY_ADDONS,
URL_ESPHOME,
URL_MUSIC_ASSISTANT,
}
class BuiltinRepository(StrEnum):
"""Built-in add-on repository."""
CORE = StoreType.CORE.value
LOCAL = StoreType.LOCAL.value
COMMUNITY_ADDONS = URL_COMMUNITY_ADDONS
ESPHOME = URL_ESPHOME
MUSIC_ASSISTANT = URL_MUSIC_ASSISTANT
def __init__(self, value: str) -> None:
"""Initialize repository item."""
if value == StoreType.LOCAL:
self.id = value
self.url = ""
self.type = StoreType.LOCAL
elif value == StoreType.CORE:
self.id = value
self.url = URL_HASSIO_ADDONS
self.type = StoreType.CORE
else:
self.id = get_hash_from_repository(value)
self.url = value
self.type = StoreType.GIT
def get_path(self, coresys: CoreSys) -> Path:
"""Get path to git repo for repository."""
if self.id == StoreType.LOCAL:
return coresys.config.path_addons_local
if self.id == StoreType.CORE:
return coresys.config.path_addons_core
return Path(coresys.config.path_addons_git, self.id)
BUILTIN_REPOSITORIES = {r.value for r in BuiltinRepository}
# pylint: disable=no-value-for-parameter
SCHEMA_REPOSITORY_CONFIG = vol.Schema(

View File

@ -409,7 +409,7 @@ async def coresys(
coresys_obj.init_websession = AsyncMock()
# Don't remove files/folders related to addons and stores
with patch("supervisor.store.git.GitRepo._remove"):
with patch("supervisor.store.git.GitRepoCustom.remove"):
yield coresys_obj
await coresys_obj.dbus.unload()

View File

@ -15,6 +15,13 @@ from supervisor.store.git import GitRepo
REPO_URL = "https://github.com/awesome-developer/awesome-repo"
class GitRepoTest(GitRepo):
"""Implementation of GitRepo for tests that allows direct setting of path."""
async def remove(self) -> None:
"""Not implemented."""
@pytest.fixture(name="clone_from")
async def fixture_clone_from():
"""Mock git clone_from."""
@ -28,7 +35,7 @@ async def test_git_clone(
):
"""Test git clone."""
fragment = f"#{branch}" if branch else ""
repo = GitRepo(coresys, tmp_path, f"{REPO_URL}{fragment}")
repo = GitRepoTest(coresys, tmp_path, f"{REPO_URL}{fragment}")
await repo.clone.__wrapped__(repo)
@ -56,7 +63,7 @@ async def test_git_clone_error(
coresys: CoreSys, tmp_path: Path, clone_from: AsyncMock, git_error: Exception
):
"""Test git clone error."""
repo = GitRepo(coresys, tmp_path, REPO_URL)
repo = GitRepoTest(coresys, tmp_path, REPO_URL)
clone_from.side_effect = git_error
with pytest.raises(StoreGitCloneError):
@ -68,7 +75,7 @@ async def test_git_clone_error(
async def test_git_load(coresys: CoreSys, tmp_path: Path):
"""Test git load."""
repo_dir = tmp_path / "repo"
repo = GitRepo(coresys, repo_dir, REPO_URL)
repo = GitRepoTest(coresys, repo_dir, REPO_URL)
repo.clone = AsyncMock()
# Test with non-existing git repo root directory
@ -106,7 +113,7 @@ async def test_git_load(coresys: CoreSys, tmp_path: Path):
async def test_git_load_error(coresys: CoreSys, tmp_path: Path, git_errors: Exception):
"""Test git load error."""
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
repo = GitRepo(coresys, tmp_path, REPO_URL)
repo = GitRepoTest(coresys, tmp_path, REPO_URL)
# Pretend we have a repo
(tmp_path / ".git").mkdir()