Skip to content

Commit abc4494

Browse files
authored
Refactor addon git repo (#5987)
* Refactor Repository into setup with inheritance * Remove subclasses of GitRepo
1 parent 3e20a09 commit abc4494

File tree

13 files changed

+183
-139
lines changed

13 files changed

+183
-139
lines changed

supervisor/resolution/fixups/store_execute_reset.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,8 @@ async def process_fixup(self, reference: str | None = None) -> None:
4242
_LOGGER.warning("Can't find store %s for fixup", reference)
4343
return
4444

45-
# Local add-ons are not a git repo, can't remove and re-pull
4645
try:
47-
if repository.git:
48-
await repository.git.reset()
49-
50-
# Load data again
51-
await repository.load()
46+
await repository.reset()
5247
except StoreError:
5348
raise ResolutionFixupError() from None
5449

supervisor/store/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ async def _add_repository(
135135
if url == URL_HASSIO_ADDONS:
136136
url = StoreType.CORE
137137

138-
repository = Repository(self.coresys, url)
138+
repository = Repository.create(self.coresys, url)
139139

140140
if repository.slug in self.repositories:
141141
raise StoreError(f"Can't add {url}, already in the store", _LOGGER.error)
@@ -183,7 +183,7 @@ async def _add_repository(
183183
raise err
184184

185185
else:
186-
if not await self.sys_run_in_executor(repository.validate):
186+
if not await repository.validate():
187187
if add_with_errors:
188188
_LOGGER.error("%s is not a valid add-on repository", url)
189189
self.sys_resolution.create_issue(

supervisor/store/git.py

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Init file for Supervisor add-on Git."""
22

3-
from abc import ABC, abstractmethod
43
import asyncio
54
import errno
65
import functools as ft
@@ -16,17 +15,14 @@
1615
from ..jobs.decorator import Job, JobCondition
1716
from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
1817
from ..utils import remove_folder
19-
from .utils import get_hash_from_repository
20-
from .validate import RE_REPOSITORY, BuiltinRepository
18+
from .validate import RE_REPOSITORY
2119

2220
_LOGGER: logging.Logger = logging.getLogger(__name__)
2321

2422

25-
class GitRepo(CoreSysAttributes, ABC):
23+
class GitRepo(CoreSysAttributes):
2624
"""Manage Add-on Git repository."""
2725

28-
builtin: bool
29-
3026
def __init__(self, coresys: CoreSys, path: Path, url: str):
3127
"""Initialize Git base wrapper."""
3228
self.coresys: CoreSys = coresys
@@ -239,38 +235,8 @@ async def pull(self) -> bool:
239235
)
240236
raise StoreGitError() from err
241237

242-
@abstractmethod
243238
async def remove(self) -> None:
244239
"""Remove a repository."""
245-
246-
247-
class GitRepoBuiltin(GitRepo):
248-
"""Built-in add-ons repository."""
249-
250-
builtin: bool = True
251-
252-
def __init__(self, coresys: CoreSys, repository: BuiltinRepository):
253-
"""Initialize Git Supervisor add-on repository."""
254-
super().__init__(coresys, repository.get_path(coresys), repository.url)
255-
256-
async def remove(self) -> None:
257-
"""Raise. Cannot remove built-in repositories."""
258-
raise RuntimeError("Cannot remove built-in repositories!")
259-
260-
261-
class GitRepoCustom(GitRepo):
262-
"""Custom add-ons repository."""
263-
264-
builtin: bool = False
265-
266-
def __init__(self, coresys, url):
267-
"""Initialize custom Git Supervisor addo-n repository."""
268-
path = Path(coresys.config.path_addons_git, get_hash_from_repository(url))
269-
270-
super().__init__(coresys, path, url)
271-
272-
async def remove(self) -> None:
273-
"""Remove a custom repository."""
274240
if self.lock.locked():
275241
_LOGGER.warning(
276242
"Cannot remove add-on repository %s, there is already a task in progress",

supervisor/store/repository.py

Lines changed: 134 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""Represent a Supervisor repository."""
22

3+
from __future__ import annotations
4+
5+
from abc import ABC, abstractmethod
36
import logging
47
from pathlib import Path
58

@@ -12,36 +15,32 @@
1215
from ..exceptions import ConfigurationFileError, StoreError
1316
from ..utils.common import read_json_or_yaml_file
1417
from .const import StoreType
15-
from .git import GitRepo, GitRepoBuiltin, GitRepoCustom
18+
from .git import GitRepo
1619
from .utils import get_hash_from_repository
1720
from .validate import SCHEMA_REPOSITORY_CONFIG, BuiltinRepository
1821

1922
_LOGGER: logging.Logger = logging.getLogger(__name__)
2023
UNKNOWN = "unknown"
2124

2225

23-
class Repository(CoreSysAttributes):
26+
class Repository(CoreSysAttributes, ABC):
2427
"""Add-on store repository in Supervisor."""
2528

2629
def __init__(self, coresys: CoreSys, repository: str):
2730
"""Initialize add-on store repository object."""
31+
self._slug: str
32+
self._type: StoreType
2833
self.coresys: CoreSys = coresys
29-
self.git: GitRepo | None = None
30-
3134
self.source: str = repository
35+
36+
@staticmethod
37+
def create(coresys: CoreSys, repository: str) -> Repository:
38+
"""Create a repository instance."""
3239
if repository == StoreType.LOCAL:
33-
self._slug = repository
34-
self._type = StoreType.LOCAL
35-
self._latest_mtime: float | None = None
36-
elif repository in BuiltinRepository:
37-
builtin = BuiltinRepository(repository)
38-
self.git = GitRepoBuiltin(coresys, builtin)
39-
self._slug = builtin.id
40-
self._type = builtin.type
41-
else:
42-
self.git = GitRepoCustom(coresys, repository)
43-
self._slug = get_hash_from_repository(repository)
44-
self._type = StoreType.GIT
40+
return RepositoryLocal(coresys)
41+
if repository in BuiltinRepository:
42+
return RepositoryGitBuiltin(coresys, BuiltinRepository(repository))
43+
return RepositoryCustom(coresys, repository)
4544

4645
def __repr__(self) -> str:
4746
"""Return internal representation."""
@@ -77,52 +76,117 @@ def maintainer(self) -> str:
7776
"""Return url of repository."""
7877
return self.data.get(ATTR_MAINTAINER, UNKNOWN)
7978

80-
def validate(self) -> bool:
81-
"""Check if store is valid.
79+
@abstractmethod
80+
async def validate(self) -> bool:
81+
"""Check if store is valid."""
82+
83+
@abstractmethod
84+
async def load(self) -> None:
85+
"""Load addon repository."""
86+
87+
@abstractmethod
88+
async def update(self) -> bool:
89+
"""Update add-on repository.
8290
83-
Must be run in executor.
91+
Returns True if the repository was updated.
8492
"""
85-
if not self.git or self.type == StoreType.CORE:
86-
return True
8793

88-
# If exists?
89-
for filetype in FILE_SUFFIX_CONFIGURATION:
90-
repository_file = Path(self.git.path / f"repository{filetype}")
91-
if repository_file.exists():
92-
break
94+
@abstractmethod
95+
async def remove(self) -> None:
96+
"""Remove add-on repository."""
9397

94-
if not repository_file.exists():
95-
return False
98+
@abstractmethod
99+
async def reset(self) -> None:
100+
"""Reset add-on repository to fix corruption issue with files."""
96101

97-
# If valid?
98-
try:
99-
SCHEMA_REPOSITORY_CONFIG(read_json_or_yaml_file(repository_file))
100-
except (ConfigurationFileError, vol.Invalid) as err:
101-
_LOGGER.warning("Could not validate repository configuration %s", err)
102-
return False
103102

103+
class RepositoryBuiltin(Repository, ABC):
104+
"""A built-in add-on repository."""
105+
106+
def __init__(self, coresys: CoreSys, builtin: BuiltinRepository) -> None:
107+
"""Initialize object."""
108+
super().__init__(coresys, builtin.value)
109+
self._builtin = builtin
110+
self._slug = builtin.id
111+
self._type = builtin.type
112+
113+
async def validate(self) -> bool:
114+
"""Assume built-in repositories are always valid."""
104115
return True
105116

117+
async def remove(self) -> None:
118+
"""Raise. Not supported for built-in repositories."""
119+
raise StoreError("Can't remove built-in repositories!", _LOGGER.error)
120+
121+
122+
class RepositoryGit(Repository, ABC):
123+
"""A git based add-on repository."""
124+
125+
_git: GitRepo
126+
106127
async def load(self) -> None:
107128
"""Load addon repository."""
108-
if not self.git:
109-
self._latest_mtime, _ = await self.sys_run_in_executor(
110-
get_latest_mtime, self.sys_config.path_addons_local
111-
)
112-
return
113-
await self.git.load()
129+
await self._git.load()
114130

115131
async def update(self) -> bool:
116132
"""Update add-on repository.
117133
118134
Returns True if the repository was updated.
119135
"""
120-
if not await self.sys_run_in_executor(self.validate):
136+
if not await self.validate():
121137
return False
122138

123-
if self.git:
124-
return await self.git.pull()
139+
return await self._git.pull()
140+
141+
async def validate(self) -> bool:
142+
"""Check if store is valid."""
143+
144+
def validate_file() -> bool:
145+
# If exists?
146+
for filetype in FILE_SUFFIX_CONFIGURATION:
147+
repository_file = Path(self._git.path / f"repository{filetype}")
148+
if repository_file.exists():
149+
break
150+
151+
if not repository_file.exists():
152+
return False
153+
154+
# If valid?
155+
try:
156+
SCHEMA_REPOSITORY_CONFIG(read_json_or_yaml_file(repository_file))
157+
except (ConfigurationFileError, vol.Invalid) as err:
158+
_LOGGER.warning("Could not validate repository configuration %s", err)
159+
return False
125160

161+
return True
162+
163+
return await self.sys_run_in_executor(validate_file)
164+
165+
async def reset(self) -> None:
166+
"""Reset add-on repository to fix corruption issue with files."""
167+
await self._git.reset()
168+
await self.load()
169+
170+
171+
class RepositoryLocal(RepositoryBuiltin):
172+
"""A local add-on repository."""
173+
174+
def __init__(self, coresys: CoreSys) -> None:
175+
"""Initialize object."""
176+
super().__init__(coresys, BuiltinRepository.LOCAL)
177+
self._latest_mtime: float | None = None
178+
179+
async def load(self) -> None:
180+
"""Load addon repository."""
181+
self._latest_mtime, _ = await self.sys_run_in_executor(
182+
get_latest_mtime, self.sys_config.path_addons_local
183+
)
184+
185+
async def update(self) -> bool:
186+
"""Update add-on repository.
187+
188+
Returns True if the repository was updated.
189+
"""
126190
# Check local modifications
127191
latest_mtime, modified_path = await self.sys_run_in_executor(
128192
get_latest_mtime, self.sys_config.path_addons_local
@@ -138,9 +202,32 @@ async def update(self) -> bool:
138202

139203
return False
140204

205+
async def reset(self) -> None:
206+
"""Raise. Not supported for local repository."""
207+
raise StoreError(
208+
"Can't reset local repository as it is not git based!", _LOGGER.error
209+
)
210+
211+
212+
class RepositoryGitBuiltin(RepositoryBuiltin, RepositoryGit):
213+
"""A built-in add-on repository based on git."""
214+
215+
def __init__(self, coresys: CoreSys, builtin: BuiltinRepository) -> None:
216+
"""Initialize object."""
217+
super().__init__(coresys, builtin)
218+
self._git = GitRepo(coresys, builtin.get_path(coresys), builtin.url)
219+
220+
221+
class RepositoryCustom(RepositoryGit):
222+
"""A custom add-on repository."""
223+
224+
def __init__(self, coresys: CoreSys, url: str) -> None:
225+
"""Initialize object."""
226+
super().__init__(coresys, url)
227+
self._slug = get_hash_from_repository(url)
228+
self._type = StoreType.GIT
229+
self._git = GitRepo(coresys, coresys.config.path_addons_git / self._slug, url)
230+
141231
async def remove(self) -> None:
142232
"""Remove add-on repository."""
143-
if not self.git or self.git.builtin:
144-
raise StoreError("Can't remove built-in repositories!", _LOGGER.error)
145-
146-
await self.git.remove()
233+
await self._git.remove()

tests/addons/test_addon.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,7 @@ async def test_paths_cache(coresys: CoreSys, install_addon_ssh: Addon):
820820

821821
with (
822822
patch("supervisor.addons.addon.Path.exists", return_value=True),
823-
patch("supervisor.store.repository.Repository.update", return_value=True),
823+
patch("supervisor.store.repository.RepositoryLocal.update", return_value=True),
824824
):
825825
await coresys.store.reload(coresys.store.get("local"))
826826

tests/addons/test_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
3030
from supervisor.resolution.data import Issue, Suggestion
3131
from supervisor.store.addon import AddonStore
32-
from supervisor.store.repository import Repository
32+
from supervisor.store.repository import RepositoryLocal
3333
from supervisor.utils import check_exception_chain
3434
from supervisor.utils.common import write_json_file
3535

@@ -442,7 +442,7 @@ async def mock_update(_, version, image, *args, **kwargs):
442442
update_task = coresys.create_task(simulate_update())
443443
await asyncio.sleep(0)
444444

445-
with patch.object(Repository, "update", return_value=True):
445+
with patch.object(RepositoryLocal, "update", return_value=True):
446446
await coresys.store.reload()
447447

448448
assert "image" not in coresys.store.data.addons["local_ssh"]

tests/api/test_store.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ async def test_api_store_add_repository(
9797
) -> None:
9898
"""Test POST /store/repositories REST API."""
9999
with (
100-
patch("supervisor.store.repository.Repository.load", return_value=None),
101-
patch("supervisor.store.repository.Repository.validate", return_value=True),
100+
patch("supervisor.store.repository.RepositoryGit.load", return_value=None),
101+
patch("supervisor.store.repository.RepositoryGit.validate", return_value=True),
102102
):
103103
response = await api_client.post(
104104
"/store/repositories", json={"repository": REPO_URL}

0 commit comments

Comments
 (0)