Skip to content

Commit

Permalink
Use new API for integration manifest (#2403)
Browse files Browse the repository at this point in the history
  • Loading branch information
ludeeus committed Dec 28, 2021
1 parent 31a3068 commit 0d23863
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 166 deletions.
2 changes: 1 addition & 1 deletion action/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import logging
import os

import aiohttp
from aiogithubapi import GitHub, GitHubAPI
import aiohttp
from homeassistant.core import HomeAssistant

from custom_components.hacs.base import HacsBase
Expand Down
4 changes: 1 addition & 3 deletions custom_components/hacs/repositories/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,14 +621,12 @@ async def download_content(self) -> None:

async def async_get_hacs_json(self, ref: str = None) -> dict[str, Any] | None:
"""Get the content of the hacs.json file."""
ref = ref or version_to_download(self)

try:
response = await self.hacs.async_github_api_method(
method=self.hacs.githubapi.repos.contents.get,
repository=self.data.full_name,
path=RepositoryFile.HACS_JSON,
**{"params": {"ref": ref}},
**{"params": {"ref": ref or version_to_download(self)}},
)
return json.loads(decode_content(response.data.content))
except BaseException: # pylint: disable=broad-except
Expand Down
84 changes: 68 additions & 16 deletions custom_components/hacs/repositories/integration.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
"""Class for integrations in HACS."""
from __future__ import annotations

from typing import TYPE_CHECKING
import json
from typing import TYPE_CHECKING, Any

from homeassistant.loader import async_get_custom_components

from ..enums import HacsCategory, HacsGitHubRepo
from ..enums import HacsCategory, HacsGitHubRepo, RepositoryFile
from ..exceptions import HacsException
from ..utils import filters
from ..utils.information import get_integration_manifest
from ..utils.decode import decode_content
from ..utils.filters import get_first_directory_in_directory
from ..utils.version import version_to_download
from .base import HacsRepository

if TYPE_CHECKING:
Expand Down Expand Up @@ -51,19 +53,32 @@ async def validate_repository(self):
self.content.path.remote = ""

if self.content.path.remote == "custom_components":
name = filters.get_first_directory_in_directory(self.tree, "custom_components")
name = get_first_directory_in_directory(self.tree, "custom_components")
if name is None:
raise HacsException(
f"Repository structure for {self.ref.replace('tags/','')} is not compliant"
)
self.content.path.remote = f"custom_components/{name}"

try:
await get_integration_manifest(self)
except HacsException as exception:
if self.hacs.system.action:
raise HacsException(f"::error:: {exception}") from exception
self.logger.error("%s %s", self, exception)
# Get the content of manifest.json
if manifest := await self.async_get_integration_manifest():
try:
self.integration_manifest = manifest
self.data.authors = manifest["codeowners"]
self.data.domain = manifest["domain"]
self.data.manifest_name = manifest["name"]
self.data.config_flow = manifest.get("config_flow", False)

# Set local path
self.content.path.local = self.localpath

except KeyError as exception:
self.validate.errors.append(
f"Missing expected key '{exception}' in { RepositoryFile.MAINIFEST_JSON}"
)
self.hacs.log.error(
"Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON
)

# Handle potential errors
if self.validate.errors:
Expand All @@ -81,13 +96,28 @@ async def update_repository(self, ignore_issues=False, force=False):
self.content.path.remote = ""

if self.content.path.remote == "custom_components":
name = filters.get_first_directory_in_directory(self.tree, "custom_components")
name = get_first_directory_in_directory(self.tree, "custom_components")
self.content.path.remote = f"custom_components/{name}"

try:
await get_integration_manifest(self)
except HacsException as exception:
self.logger.error("%s %s", self, exception)
# Get the content of manifest.json
if manifest := await self.async_get_integration_manifest():
try:
self.integration_manifest = manifest
self.data.authors = manifest["codeowners"]
self.data.domain = manifest["domain"]
self.data.manifest_name = manifest["name"]
self.data.config_flow = manifest.get("config_flow", False)

# Set local path
self.content.path.local = self.localpath

except KeyError as exception:
self.validate.errors.append(
f"Missing expected key '{exception}' in { RepositoryFile.MAINIFEST_JSON}"
)
self.hacs.log.error(
"Missing expected key '%s' in '%s'", exception, RepositoryFile.MAINIFEST_JSON
)

# Set local path
self.content.path.local = self.localpath
Expand All @@ -98,3 +128,25 @@ async def reload_custom_components(self):
del self.hacs.hass.data["custom_components"]
await async_get_custom_components(self.hacs.hass)
self.logger.info("Custom_component cache reloaded")

async def async_get_integration_manifest(self, ref: str = None) -> dict[str, Any] | None:
"""Get the content of the manifest.json file."""
manifest_path = (
"manifest.json"
if self.data.content_in_root
else f"{self.content.path.remote}/{RepositoryFile.MAINIFEST_JSON}"
)

if not manifest_path in (x.full_path for x in self.tree):
raise HacsException(f"No {RepositoryFile.MAINIFEST_JSON} file found '{manifest_path}'")

try:
response = await self.hacs.async_github_api_method(
method=self.hacs.githubapi.repos.contents.get,
repository=self.data.full_name,
path=manifest_path,
**{"params": {"ref": ref or version_to_download(self)}},
)
return json.loads(decode_content(response.data.content))
except BaseException as err: # pylint: disable=broad-except
pass
30 changes: 0 additions & 30 deletions custom_components/hacs/utils/information.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""Return repository information if any."""
import json

from aiogithubapi import AIOGitHubAPIException

from ..exceptions import HacsException
Expand All @@ -22,31 +20,3 @@ async def get_releases(repository, prerelease=False, returnlimit=5):
return releases
except (ValueError, AIOGitHubAPIException) as exception:
raise HacsException(exception)


async def get_integration_manifest(repository):
"""Return the integration manifest."""
if repository.data.content_in_root:
manifest_path = "manifest.json"
else:
manifest_path = f"{repository.content.path.remote}/manifest.json"
if not manifest_path in [x.full_path for x in repository.tree]:
raise HacsException(f"No file found '{manifest_path}'")
try:
manifest = await repository.repository_object.get_contents(manifest_path, repository.ref)
manifest = json.loads(manifest.content)
except BaseException as exception: # pylint: disable=broad-except
raise HacsException(f"Could not read manifest.json [{exception}]")

try:
repository.integration_manifest = manifest
repository.data.authors = manifest["codeowners"]
repository.data.domain = manifest["domain"]
repository.data.manifest_name = manifest["name"]
repository.data.config_flow = manifest.get("config_flow", False)

# Set local path
repository.content.path.local = repository.localpath

except KeyError as exception:
raise HacsException(f"Missing expected key {exception} in '{manifest_path}'") from exception
27 changes: 19 additions & 8 deletions tests/common.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# pylint: disable=missing-docstring,invalid-name
from __future__ import annotations

import asyncio
from contextlib import contextmanager
import functools as ft
Expand Down Expand Up @@ -27,29 +29,38 @@

def fixture(filename, asjson=True):
"""Load a fixture."""
path = os.path.join(os.path.dirname(__file__), "fixtures", filename)
with open(path, encoding="utf-8") as fptr:
if asjson:
return json.loads(fptr.read())
return fptr.read()
filename = f"{filename}.json" if "." not in filename else filename
path = os.path.join(
os.path.dirname(__file__),
"fixtures",
filename.lower().replace("/", "_"),
)
try:
with open(path, encoding="utf-8") as fptr:
_LOGGER.debug("Loading fixture from %s", path)
if asjson:
return json.loads(fptr.read())
return fptr.read()
except OSError as err:
raise OSError(f"Missing fixture for {path.split('fixtures/')[1]}") from err


def dummy_repository_base(hacs, repository=None):
if repository is None:
repository = HacsRepository(hacs)
repository.data.full_name = "test/test"
repository.data.full_name_lower = "test/test"
repository.hacs = hacs
repository.hacs.hass = hacs.hass
repository.hacs.core.config_path = hacs.hass.config.path()
repository.logger = get_hacs_logger()
repository.data.full_name = "test/test"
repository.data.full_name_lower = "test/test"
repository.data.domain = "test"
repository.data.last_version = "3"
repository.data.selected_tag = "3"
repository.ref = version_to_download(repository)
repository.integration_manifest = {"config_flow": False, "domain": "test"}
repository.data.published_tags = ["1", "2", "3"]
repository.data.update_data(fixture("repository_data.json"))
repository.data.update_data(fixture("repository_data.json", asjson=True))

async def update_repository():
pass
Expand Down
3 changes: 0 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,6 @@ def hacs(hass: HomeAssistant):
hacs_obj.version = hacs_obj.integration.version
hacs_obj.configuration.token = TOKEN

if not "PYTEST" in os.environ and "GITHUB_ACTION" in os.environ:
hacs_obj.system.action = True

hass.data[DOMAIN] = hacs_obj

yield hacs_obj
Expand Down
7 changes: 3 additions & 4 deletions tests/helpers/functions/test_logger.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import os
from logging import Logger

from custom_components.hacs.utils.logger import get_hacs_logger


def test_logger():
os.environ["GITHUB_ACTION"] = "value"
get_hacs_logger()
del os.environ["GITHUB_ACTION"]
hacs_logger = get_hacs_logger()
assert isinstance(hacs_logger, Logger)
103 changes: 2 additions & 101 deletions tests/helpers/information/test_get_integration_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@
import json

from aiogithubapi.objects.repository.content import AIOGitHubAPIRepositoryTreeContent
import aiohttp
import pytest

from custom_components.hacs.exceptions import HacsException
from custom_components.hacs.utils.information import get_integration_manifest

from tests.common import TOKEN
from tests.sample_data import (
integration_manifest,
repository_data,
Expand Down Expand Up @@ -61,7 +58,7 @@ async def test_get_integration_manifest(repository_integration, aresponses):
"main",
)
]
await get_integration_manifest(repository_integration)
await repository_integration.async_get_integration_manifest()
assert repository_integration.data.domain == integration_manifest["domain"]


Expand All @@ -86,100 +83,4 @@ async def test_get_integration_manifest_no_file(repository_integration, arespons
) = await repository_integration.async_get_legacy_repository_object()
repository_integration.content.path.remote = "custom_components/test"
with pytest.raises(HacsException):
await get_integration_manifest(repository_integration)


@pytest.mark.asyncio
async def test_get_integration_manifest_format_issue(repository_integration, aresponses):
aresponses.add(
"api.github.com",
"/rate_limit",
"get",
aresponses.Response(body=b"{}", headers=response_rate_limit_header),
)
aresponses.add(
"api.github.com",
"/repos/test/test",
"get",
aresponses.Response(body=json.dumps(repository_data), headers=response_rate_limit_header),
)
aresponses.add(
"api.github.com",
"/rate_limit",
"get",
aresponses.Response(body=b"{}", headers=response_rate_limit_header),
)
aresponses.add(
"api.github.com",
"/repos/test/test/contents/custom_components/test/manifest.json",
"get",
aresponses.Response(
body=json.dumps({"content": {"wrong": "format"}}),
headers=response_rate_limit_header,
),
)

(
repository_integration.repository_object,
_,
) = await repository_integration.async_get_legacy_repository_object()
repository_integration.content.path.remote = "custom_components/test"
repository_integration.tree = [
AIOGitHubAPIRepositoryTreeContent(
{"path": "custom_components/test/manifest.json", "type": "blob"},
"test/test",
"main",
)
]
with pytest.raises(HacsException):
await get_integration_manifest(repository_integration)


@pytest.mark.asyncio
async def test_get_integration_manifest_missing_required_key(
repository_integration, aresponses, event_loop
):
aresponses.add(
"api.github.com",
"/rate_limit",
"get",
aresponses.Response(body=b"{}", headers=response_rate_limit_header),
)
aresponses.add(
"api.github.com",
"/repos/test/test",
"get",
aresponses.Response(body=json.dumps(repository_data), headers=response_rate_limit_header),
)
aresponses.add(
"api.github.com",
"/rate_limit",
"get",
aresponses.Response(body=b"{}", headers=response_rate_limit_header),
)
del integration_manifest["domain"]
content = base64.b64encode(json.dumps(integration_manifest).encode("utf-8"))
aresponses.add(
"api.github.com",
"/repos/test/test/contents/custom_components/test/manifest.json",
"get",
aresponses.Response(
body=json.dumps({"content": content.decode("utf-8")}),
headers=response_rate_limit_header,
),
)

(
repository_integration.repository_object,
_,
) = await repository_integration.async_get_legacy_repository_object()
repository_integration.content.path.remote = "custom_components/test"
repository_integration.tree = [
AIOGitHubAPIRepositoryTreeContent(
{"path": "custom_components/test/manifest.json", "type": "blob"},
"test/test",
"main",
)
]
with pytest.raises(HacsException):
await get_integration_manifest(repository_integration)
await repository_integration.async_get_integration_manifest()

0 comments on commit 0d23863

Please sign in to comment.