Drop ensure_builtin_repositories() (#6012)

* Drop ensure_builtin_repositories

With the new Repository classes we have the is_builtin property, so we
can easily make sure that built-ins are not removed. This allows us to
further cleanup the code by removing the ensure_builtin_repositories
function and the ALL_BUILTIN_REPOSITORIES constant.

* Make sure we add built-ins on load

* Reuse default set and avoid unnecessary copy

Reuse default set and avoid unnecessary copying during validation if
the default is not being used.
This commit is contained in:
Stefan Agner 2025-07-14 22:19:06 +02:00 committed by GitHub
parent eefe2f2e06
commit 99c040520e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 39 additions and 45 deletions

View File

@ -931,5 +931,5 @@ class Backup(JobGroup):
Return a coroutine. Return a coroutine.
""" """
return self.sys_store.update_repositories( return self.sys_store.update_repositories(
self.repositories, issue_on_error=True, replace=replace set(self.repositories), issue_on_error=True, replace=replace
) )

View File

@ -21,7 +21,7 @@ from .addon import AddonStore
from .const import FILE_HASSIO_STORE, BuiltinRepository from .const import FILE_HASSIO_STORE, BuiltinRepository
from .data import StoreData from .data import StoreData
from .repository import Repository from .repository import Repository
from .validate import SCHEMA_STORE_FILE, ensure_builtin_repositories from .validate import DEFAULT_REPOSITORIES, SCHEMA_STORE_FILE
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -63,11 +63,14 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
return self.repositories[slug] return self.repositories[slug]
async def load(self) -> None: async def load(self) -> None:
"""Start up add-on management.""" """Start up add-on store management."""
# Init custom repositories and load add-ons # Make sure the built-in repositories are all present
await self.update_repositories( # This is especially important when adding new built-in repositories
self._data[ATTR_REPOSITORIES], issue_on_error=True # to make sure existing installations have them.
all_repositories: set[str] = (
set(self._data.get(ATTR_REPOSITORIES, [])) | DEFAULT_REPOSITORIES
) )
await self.update_repositories(all_repositories, issue_on_error=True)
@Job( @Job(
name="store_manager_reload", name="store_manager_reload",
@ -223,7 +226,7 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
@Job(name="store_manager_update_repositories") @Job(name="store_manager_update_repositories")
async def update_repositories( async def update_repositories(
self, self,
list_repositories: list[str], list_repositories: set[str],
*, *,
issue_on_error: bool = False, issue_on_error: bool = False,
replace: bool = True, replace: bool = True,
@ -231,14 +234,8 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
"""Update repositories by adding new ones and removing stale ones.""" """Update repositories by adding new ones and removing stale ones."""
current_repositories = {repository.source for repository in self.all} current_repositories = {repository.source for repository in self.all}
# Determine changes needed # Determine repositories to add
if replace: repositories_to_add = list_repositories - current_repositories
target_repositories = set(ensure_builtin_repositories(list_repositories))
repositories_to_add = target_repositories - current_repositories
else:
# When not replacing, just add the new repositories
repositories_to_add = set(list_repositories) - current_repositories
target_repositories = current_repositories | repositories_to_add
# Add new repositories # Add new repositories
add_errors = await asyncio.gather( add_errors = await asyncio.gather(
@ -259,7 +256,7 @@ class StoreManager(CoreSysAttributes, FileConfiguration):
repositories_to_remove: list[Repository] = [ repositories_to_remove: list[Repository] = [
repository repository
for repository in self.all for repository in self.all
if repository.source not in target_repositories if repository.source not in list_repositories
and not repository.is_builtin and not repository.is_builtin
] ]

View File

@ -35,7 +35,3 @@ class BuiltinRepository(StrEnum):
return URL_HASSIO_ADDONS return URL_HASSIO_ADDONS
else: else:
return self.value # For URL-based repos, value is the URL return self.value # For URL-based repos, value is the URL
# All repositories that are considered "built-in" and protected from removal
ALL_BUILTIN_REPOSITORIES = {repo.value for repo in BuiltinRepository}

View File

@ -4,7 +4,7 @@ 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
from ..validate import RE_REPOSITORY from ..validate import RE_REPOSITORY
from .const import ALL_BUILTIN_REPOSITORIES, BuiltinRepository from .const import BuiltinRepository
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
SCHEMA_REPOSITORY_CONFIG = vol.Schema( SCHEMA_REPOSITORY_CONFIG = vol.Schema(
@ -17,15 +17,6 @@ SCHEMA_REPOSITORY_CONFIG = vol.Schema(
) )
def ensure_builtin_repositories(addon_repositories: list[str]) -> list[str]:
"""Ensure builtin repositories are in list.
Note: This should not be used in validation as the resulting list is not
stable. This can have side effects when comparing data later on.
"""
return list(set(addon_repositories) | ALL_BUILTIN_REPOSITORIES)
def validate_repository(repository: str) -> str: def validate_repository(repository: str) -> str:
"""Validate a valid repository.""" """Validate a valid repository."""
if repository in BuiltinRepository: if repository in BuiltinRepository:
@ -44,10 +35,12 @@ def validate_repository(repository: str) -> str:
repositories = vol.All([validate_repository], vol.Unique()) repositories = vol.All([validate_repository], vol.Unique())
DEFAULT_REPOSITORIES = {repo.value for repo in BuiltinRepository}
SCHEMA_STORE_FILE = vol.Schema( SCHEMA_STORE_FILE = vol.Schema(
{ {
vol.Optional( vol.Optional(
ATTR_REPOSITORIES, default=list(ALL_BUILTIN_REPOSITORIES) ATTR_REPOSITORIES, default=lambda: list(DEFAULT_REPOSITORIES)
): repositories, ): repositories,
}, },
extra=vol.REMOVE_EXTRA, extra=vol.REMOVE_EXTRA,

View File

@ -17,7 +17,7 @@ from supervisor.exceptions import (
from supervisor.resolution.const import SuggestionType from supervisor.resolution.const import SuggestionType
from supervisor.store import StoreManager from supervisor.store import StoreManager
from supervisor.store.addon import AddonStore from supervisor.store.addon import AddonStore
from supervisor.store.const import ALL_BUILTIN_REPOSITORIES from supervisor.store.const import BuiltinRepository
from supervisor.store.repository import Repository from supervisor.store.repository import Repository
@ -50,7 +50,9 @@ async def test_add_valid_repository(
patch("pathlib.Path.exists", return_value=True), patch("pathlib.Path.exists", return_value=True),
): ):
if use_update: if use_update:
await store_manager.update_repositories(current + ["http://example.com"]) await store_manager.update_repositories(
set(current) | {"http://example.com"}
)
else: else:
await store_manager.add_repository("http://example.com") await store_manager.add_repository("http://example.com")
@ -70,7 +72,7 @@ async def test_add_invalid_repository(coresys: CoreSys, store_manager: StoreMana
), ),
): ):
await store_manager.update_repositories( await store_manager.update_repositories(
current + ["http://example.com"], issue_on_error=True set(current) | {"http://example.com"}, issue_on_error=True
) )
assert not await get_repository_by_url( assert not await get_repository_by_url(
@ -96,7 +98,9 @@ async def test_error_on_invalid_repository(
pytest.raises(StoreError), pytest.raises(StoreError),
): ):
if use_update: if use_update:
await store_manager.update_repositories(current + ["http://example.com"]) await store_manager.update_repositories(
set(current) | {"http://example.com"}
)
else: else:
await store_manager.add_repository("http://example.com") await store_manager.add_repository("http://example.com")
@ -118,7 +122,7 @@ async def test_add_invalid_repository_file(
patch("pathlib.Path.exists", return_value=False), patch("pathlib.Path.exists", return_value=False),
): ):
await store_manager.update_repositories( await store_manager.update_repositories(
current + ["http://example.com"], issue_on_error=True set(current) | {"http://example.com"}, issue_on_error=True
) )
assert not await get_repository_by_url( assert not await get_repository_by_url(
@ -146,7 +150,7 @@ async def test_add_repository_with_git_error(
current = coresys.store.repository_urls current = coresys.store.repository_urls
with patch("supervisor.store.git.GitRepo.load", side_effect=git_error): with patch("supervisor.store.git.GitRepo.load", side_effect=git_error):
await store_manager.update_repositories( await store_manager.update_repositories(
current + ["http://example.com"], issue_on_error=True set(current) | {"http://example.com"}, issue_on_error=True
) )
assert "http://example.com" in coresys.store.repository_urls assert "http://example.com" in coresys.store.repository_urls
@ -175,7 +179,9 @@ async def test_error_on_repository_with_git_error(
pytest.raises(StoreError), pytest.raises(StoreError),
): ):
if use_update: if use_update:
await store_manager.update_repositories(current + ["http://example.com"]) await store_manager.update_repositories(
set(current) | {"http://example.com"}
)
else: else:
await store_manager.add_repository("http://example.com") await store_manager.add_repository("http://example.com")
@ -189,7 +195,9 @@ async def test_preinstall_valid_repository(
): ):
"""Test add core repository valid.""" """Test add core repository valid."""
with patch("supervisor.store.git.GitRepo.load", return_value=None): with patch("supervisor.store.git.GitRepo.load", return_value=None):
await store_manager.update_repositories(list(ALL_BUILTIN_REPOSITORIES)) await store_manager.update_repositories(
{repo.value for repo in BuiltinRepository}
)
def validate(): def validate():
assert store_manager.get("core").validate() assert store_manager.get("core").validate()
@ -213,7 +221,7 @@ async def test_remove_repository(
assert test_repository.slug in coresys.store.repositories assert test_repository.slug in coresys.store.repositories
if use_update: if use_update:
await store_manager.update_repositories([]) await store_manager.update_repositories(set())
else: else:
await store_manager.remove_repository(test_repository) await store_manager.remove_repository(test_repository)
@ -241,7 +249,7 @@ async def test_remove_used_repository(
match="Can't remove 'https://github.com/awesome-developer/awesome-repo'. It's used by installed add-ons", match="Can't remove 'https://github.com/awesome-developer/awesome-repo'. It's used by installed add-ons",
): ):
if use_update: if use_update:
await store_manager.update_repositories([]) await store_manager.update_repositories(set())
else: else:
await store_manager.remove_repository( await store_manager.remove_repository(
coresys.store.repositories[store_addon.repository] coresys.store.repositories[store_addon.repository]
@ -252,7 +260,7 @@ async def test_update_partial_error(coresys: CoreSys, store_manager: StoreManage
"""Test partial error on update does partial save and errors.""" """Test partial error on update does partial save and errors."""
with patch("supervisor.store.repository.RepositoryGit.validate", return_value=True): with patch("supervisor.store.repository.RepositoryGit.validate", return_value=True):
with patch("supervisor.store.git.GitRepo.load", return_value=None): with patch("supervisor.store.git.GitRepo.load", return_value=None):
await store_manager.update_repositories([]) await store_manager.update_repositories(set())
store_manager.data.update.assert_called_once() store_manager.data.update.assert_called_once()
store_manager.data.update.reset_mock() store_manager.data.update.reset_mock()
@ -268,7 +276,7 @@ async def test_update_partial_error(coresys: CoreSys, store_manager: StoreManage
pytest.raises(StoreError), pytest.raises(StoreError),
): ):
await store_manager.update_repositories( await store_manager.update_repositories(
current + ["http://example.com", "http://example2.com"] set(current) | {"http://example.com", "http://example2.com"}
) )
assert len(coresys.store.repository_urls) == initial + 1 assert len(coresys.store.repository_urls) == initial + 1
@ -303,7 +311,7 @@ async def test_add_with_update_repositories(
), ),
patch("pathlib.Path.exists", return_value=True), patch("pathlib.Path.exists", return_value=True),
): ):
await store_manager.update_repositories(["http://example.com"], replace=False) await store_manager.update_repositories({"http://example.com"}, replace=False)
assert test_repository.source in coresys.store.repository_urls assert test_repository.source in coresys.store.repository_urls
assert "http://example.com" in coresys.store.repository_urls assert "http://example.com" in coresys.store.repository_urls
@ -322,7 +330,7 @@ async def test_add_repository_fails_if_out_of_date(
): ):
if use_update: if use_update:
await store_manager.update_repositories( await store_manager.update_repositories(
coresys.store.repository_urls + ["http://example.com"], set(coresys.store.repository_urls) | {"http://example.com"}
) )
else: else:
await store_manager.add_repository("http://example.com") await store_manager.add_repository("http://example.com")