Add warning to custom integrations without version (#45919)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Joakim Sørensen 2021-02-04 09:59:41 +01:00 committed by GitHub
parent 8256acb8ef
commit 06e6005fbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 173 additions and 20 deletions

View File

@ -26,6 +26,9 @@ from typing import (
cast, cast,
) )
from awesomeversion import AwesomeVersion
from awesomeversion.strategy import AwesomeVersionStrategy
from homeassistant.generated.dhcp import DHCP from homeassistant.generated.dhcp import DHCP
from homeassistant.generated.mqtt import MQTT from homeassistant.generated.mqtt import MQTT
from homeassistant.generated.ssdp import SSDP from homeassistant.generated.ssdp import SSDP
@ -52,7 +55,19 @@ CUSTOM_WARNING = (
"You are using a custom integration %s which has not " "You are using a custom integration %s which has not "
"been tested by Home Assistant. This component might " "been tested by Home Assistant. This component might "
"cause stability problems, be sure to disable it if you " "cause stability problems, be sure to disable it if you "
"experience issues with Home Assistant." "experience issues with Home Assistant"
)
CUSTOM_WARNING_VERSION_MISSING = (
"No 'version' key in the manifest file for "
"custom integration '%s'. This will not be "
"allowed in a future version of Home "
"Assistant. Please report this to the "
"maintainer of '%s'"
)
CUSTOM_WARNING_VERSION_TYPE = (
"'%s' is not a valid version for "
"custom integration '%s'. "
"Please report this to the maintainer of '%s'"
) )
_UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency _UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency
@ -83,6 +98,7 @@ class Manifest(TypedDict, total=False):
dhcp: List[Dict[str, str]] dhcp: List[Dict[str, str]]
homekit: Dict[str, List[str]] homekit: Dict[str, List[str]]
is_built_in: bool is_built_in: bool
version: str
codeowners: List[str] codeowners: List[str]
@ -417,6 +433,13 @@ class Integration:
"""Test if package is a built-in integration.""" """Test if package is a built-in integration."""
return self.pkg_path.startswith(PACKAGE_BUILTIN) return self.pkg_path.startswith(PACKAGE_BUILTIN)
@property
def version(self) -> Optional[AwesomeVersion]:
"""Return the version of the integration."""
if "version" not in self.manifest:
return None
return AwesomeVersion(self.manifest["version"])
@property @property
def all_dependencies(self) -> Set[str]: def all_dependencies(self) -> Set[str]:
"""Return all dependencies including sub-dependencies.""" """Return all dependencies including sub-dependencies."""
@ -513,7 +536,7 @@ async def async_get_integration(hass: "HomeAssistant", domain: str) -> Integrati
# components to find the integration. # components to find the integration.
integration = (await async_get_custom_components(hass)).get(domain) integration = (await async_get_custom_components(hass)).get(domain)
if integration is not None: if integration is not None:
_LOGGER.warning(CUSTOM_WARNING, domain) custom_integration_warning(integration)
cache[domain] = integration cache[domain] = integration
event.set() event.set()
return integration return integration
@ -531,6 +554,7 @@ async def async_get_integration(hass: "HomeAssistant", domain: str) -> Integrati
integration = Integration.resolve_legacy(hass, domain) integration = Integration.resolve_legacy(hass, domain)
if integration is not None: if integration is not None:
custom_integration_warning(integration)
cache[domain] = integration cache[domain] = integration
else: else:
# Remove event from cache. # Remove event from cache.
@ -605,9 +629,6 @@ def _load_file(
cache[comp_or_platform] = module cache[comp_or_platform] = module
if module.__name__.startswith(PACKAGE_CUSTOM_COMPONENTS):
_LOGGER.warning(CUSTOM_WARNING, comp_or_platform)
return module return module
except ImportError as err: except ImportError as err:
@ -756,3 +777,35 @@ def _lookup_path(hass: "HomeAssistant") -> List[str]:
if hass.config.safe_mode: if hass.config.safe_mode:
return [PACKAGE_BUILTIN] return [PACKAGE_BUILTIN]
return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]
def validate_custom_integration_version(version: str) -> bool:
"""Validate the version of custom integrations."""
return AwesomeVersion(version).strategy in (
AwesomeVersionStrategy.CALVER,
AwesomeVersionStrategy.SEMVER,
AwesomeVersionStrategy.SIMPLEVER,
AwesomeVersionStrategy.BUILDVER,
AwesomeVersionStrategy.PEP440,
)
def custom_integration_warning(integration: Integration) -> None:
"""Create logs for custom integrations."""
if not integration.pkg_path.startswith(PACKAGE_CUSTOM_COMPONENTS):
return None
_LOGGER.warning(CUSTOM_WARNING, integration.domain)
if integration.manifest.get("version") is None:
_LOGGER.warning(
CUSTOM_WARNING_VERSION_MISSING, integration.domain, integration.domain
)
else:
if not validate_custom_integration_version(integration.manifest["version"]):
_LOGGER.warning(
CUSTOM_WARNING_VERSION_TYPE,
integration.domain,
integration.manifest["version"],
integration.domain,
)

View File

@ -5,6 +5,7 @@ aiohttp_cors==0.7.0
astral==1.10.1 astral==1.10.1
async_timeout==3.0.1 async_timeout==3.0.1
attrs==19.3.0 attrs==19.3.0
awesomeversion==21.2.0
bcrypt==3.1.7 bcrypt==3.1.7
certifi>=2020.12.5 certifi>=2020.12.5
ciso8601==2.1.3 ciso8601==2.1.3

View File

@ -5,6 +5,7 @@ aiohttp==3.7.3
astral==1.10.1 astral==1.10.1
async_timeout==3.0.1 async_timeout==3.0.1
attrs==19.3.0 attrs==19.3.0
awesomeversion==21.2.0
bcrypt==3.1.7 bcrypt==3.1.7
certifi>=2020.12.5 certifi>=2020.12.5
ciso8601==2.1.3 ciso8601==2.1.3

View File

@ -13,7 +13,6 @@ pre-commit==2.10.0
pylint==2.6.0 pylint==2.6.0
astroid==2.4.2 astroid==2.4.2
pipdeptree==1.0.0 pipdeptree==1.0.0
awesomeversion==21.2.0
pylint-strict-informational==0.1 pylint-strict-informational==0.1
pytest-aiohttp==0.3.0 pytest-aiohttp==0.3.0
pytest-cov==2.10.1 pytest-cov==2.10.1

View File

@ -16,4 +16,4 @@ fi
echo "Installing development dependencies..." echo "Installing development dependencies..."
python3 -m pip install wheel --constraint homeassistant/package_constraints.txt python3 -m pip install wheel --constraint homeassistant/package_constraints.txt
python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements_test.txt) --constraint homeassistant/package_constraints.txt python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) $(grep awesomeversion requirements.txt) --constraint homeassistant/package_constraints.txt

View File

@ -2,11 +2,11 @@
from typing import Dict from typing import Dict
from urllib.parse import urlparse from urllib.parse import urlparse
from awesomeversion import AwesomeVersion
from awesomeversion.strategy import AwesomeVersionStrategy
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from homeassistant.loader import validate_custom_integration_version
from .model import Integration from .model import Integration
DOCUMENTATION_URL_SCHEMA = "https" DOCUMENTATION_URL_SCHEMA = "https"
@ -53,16 +53,9 @@ def verify_uppercase(value: str):
def verify_version(value: str): def verify_version(value: str):
"""Verify the version.""" """Verify the version."""
version = AwesomeVersion(value) if not validate_custom_integration_version(value):
if version.strategy not in [
AwesomeVersionStrategy.CALVER,
AwesomeVersionStrategy.SEMVER,
AwesomeVersionStrategy.SIMPLEVER,
AwesomeVersionStrategy.BUILDVER,
AwesomeVersionStrategy.PEP440,
]:
raise vol.Invalid( raise vol.Invalid(
f"'{version}' is not a valid version. This will cause a future version of Home Assistant to block this integration.", f"'{value}' is not a valid version. This will cause a future version of Home Assistant to block this integration.",
) )
return value return value

View File

@ -36,6 +36,7 @@ REQUIRES = [
"astral==1.10.1", "astral==1.10.1",
"async_timeout==3.0.1", "async_timeout==3.0.1",
"attrs==19.3.0", "attrs==19.3.0",
"awesomeversion==21.2.0",
"bcrypt==3.1.7", "bcrypt==3.1.7",
"certifi>=2020.12.5", "certifi>=2020.12.5",
"ciso8601==2.1.3", "ciso8601==2.1.3",

View File

@ -0,0 +1,47 @@
"""Tests for hassfest version."""
import pytest
import voluptuous as vol
from script.hassfest.manifest import (
CUSTOM_INTEGRATION_MANIFEST_SCHEMA,
validate_version,
)
from script.hassfest.model import Integration
@pytest.fixture
def integration():
"""Fixture for hassfest integration model."""
integration = Integration("")
integration.manifest = {
"domain": "test",
"documentation": "https://example.com",
"name": "test",
"codeowners": ["@awesome"],
}
return integration
def test_validate_version_no_key(integration: Integration):
"""Test validate version with no key."""
validate_version(integration)
assert (
"No 'version' key in the manifest file. This will cause a future version of Home Assistant to block this integration."
in [x.error for x in integration.warnings]
)
def test_validate_custom_integration_manifest(integration: Integration):
"""Test validate custom integration manifest."""
with pytest.raises(vol.Invalid):
integration.manifest["version"] = "lorem_ipsum"
CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest)
with pytest.raises(vol.Invalid):
integration.manifest["version"] = None
CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest)
integration.manifest["version"] = "1"
schema = CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest)
assert schema["version"] == "1"

View File

@ -130,13 +130,69 @@ async def test_custom_component_name(hass):
async def test_log_warning_custom_component(hass, caplog): async def test_log_warning_custom_component(hass, caplog):
"""Test that we log a warning when loading a custom component.""" """Test that we log a warning when loading a custom component."""
hass.components.test_standalone await loader.async_get_integration(hass, "test_standalone")
assert "You are using a custom integration test_standalone" in caplog.text assert "You are using a custom integration test_standalone" in caplog.text
await loader.async_get_integration(hass, "test") await loader.async_get_integration(hass, "test")
assert "You are using a custom integration test " in caplog.text assert "You are using a custom integration test " in caplog.text
async def test_custom_integration_missing_version(hass, caplog):
"""Test that we log a warning when custom integrations are missing a version."""
test_integration_1 = loader.Integration(
hass, "custom_components.test1", None, {"domain": "test1"}
)
test_integration_2 = loader.Integration(
hass,
"custom_components.test2",
None,
loader.manifest_from_legacy_module("test2", "custom_components.test2"),
)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {
"test1": test_integration_1,
"test2": test_integration_2,
}
await loader.async_get_integration(hass, "test1")
assert (
"No 'version' key in the manifest file for custom integration 'test1'."
in caplog.text
)
await loader.async_get_integration(hass, "test2")
assert (
"No 'version' key in the manifest file for custom integration 'test2'."
in caplog.text
)
async def test_no_version_warning_for_none_custom_integrations(hass, caplog):
"""Test that we do not log a warning when core integrations are missing a version."""
await loader.async_get_integration(hass, "hue")
assert (
"No 'version' key in the manifest file for custom integration 'hue'."
not in caplog.text
)
async def test_custom_integration_version_not_valid(hass, caplog):
"""Test that we log a warning when custom integrations have a invalid version."""
test_integration = loader.Integration(
hass, "custom_components.test", None, {"domain": "test", "version": "test"}
)
with patch("homeassistant.loader.async_get_custom_components") as mock_get:
mock_get.return_value = {"test": test_integration}
await loader.async_get_integration(hass, "test")
assert (
"'test' is not a valid version for custom integration 'test'."
in caplog.text
)
async def test_get_integration(hass): async def test_get_integration(hass):
"""Test resolving integration.""" """Test resolving integration."""
integration = await loader.async_get_integration(hass, "hue") integration = await loader.async_get_integration(hass, "hue")
@ -154,7 +210,6 @@ async def test_get_integration_legacy(hass):
async def test_get_integration_custom_component(hass, enable_custom_integrations): async def test_get_integration_custom_component(hass, enable_custom_integrations):
"""Test resolving integration.""" """Test resolving integration."""
integration = await loader.async_get_integration(hass, "test_package") integration = await loader.async_get_integration(hass, "test_package")
print(integration)
assert integration.get_component().DOMAIN == "test_package" assert integration.get_component().DOMAIN == "test_package"
assert integration.name == "Test Package" assert integration.name == "Test Package"
@ -189,6 +244,7 @@ def test_integration_properties(hass):
{"manufacturer": "Signify", "modelName": "Philips hue bridge 2015"}, {"manufacturer": "Signify", "modelName": "Philips hue bridge 2015"},
], ],
"mqtt": ["hue/discovery"], "mqtt": ["hue/discovery"],
"version": "1.0.0",
}, },
) )
assert integration.name == "Philips Hue" assert integration.name == "Philips Hue"
@ -215,6 +271,7 @@ def test_integration_properties(hass):
assert integration.dependencies == ["test-dep"] assert integration.dependencies == ["test-dep"]
assert integration.requirements == ["test-req==1.0.0"] assert integration.requirements == ["test-req==1.0.0"]
assert integration.is_built_in is True assert integration.is_built_in is True
assert integration.version == "1.0.0"
integration = loader.Integration( integration = loader.Integration(
hass, hass,
@ -233,6 +290,7 @@ def test_integration_properties(hass):
assert integration.dhcp is None assert integration.dhcp is None
assert integration.ssdp is None assert integration.ssdp is None
assert integration.mqtt is None assert integration.mqtt is None
assert integration.version is None
integration = loader.Integration( integration = loader.Integration(
hass, hass,