mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 03:37:07 +00:00
Add warning to custom integrations without version (#45919)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
8256acb8ef
commit
06e6005fbb
@ -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,
|
||||||
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
1
setup.py
1
setup.py
@ -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",
|
||||||
|
47
tests/hassfest/test_version.py
Normal file
47
tests/hassfest/test_version.py
Normal 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"
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user