mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 04:07:08 +00:00
Fix circular dependancy detection (#100458)
* Fix _async_component_dependencies Fix bug with circular dependency detection Fix bug with circular after_dependency detection Simplify interface and make the code more readable * Implement review feedback * Pass all conflicting deps to Exception * Change inner docstring Co-authored-by: Erik Montnemery <erik@montnemery.com> --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
d41144ee88
commit
e1771ae01e
@ -777,9 +777,7 @@ class Integration:
|
|||||||
return self._all_dependencies_resolved
|
return self._all_dependencies_resolved
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dependencies = await _async_component_dependencies(
|
dependencies = await _async_component_dependencies(self.hass, self)
|
||||||
self.hass, self.domain, self, set(), set()
|
|
||||||
)
|
|
||||||
dependencies.discard(self.domain)
|
dependencies.discard(self.domain)
|
||||||
self._all_dependencies = dependencies
|
self._all_dependencies = dependencies
|
||||||
self._all_dependencies_resolved = True
|
self._all_dependencies_resolved = True
|
||||||
@ -998,7 +996,7 @@ class IntegrationNotLoaded(LoaderError):
|
|||||||
class CircularDependency(LoaderError):
|
class CircularDependency(LoaderError):
|
||||||
"""Raised when a circular dependency is found when resolving components."""
|
"""Raised when a circular dependency is found when resolving components."""
|
||||||
|
|
||||||
def __init__(self, from_domain: str, to_domain: str) -> None:
|
def __init__(self, from_domain: str | set[str], to_domain: str) -> None:
|
||||||
"""Initialize circular dependency error."""
|
"""Initialize circular dependency error."""
|
||||||
super().__init__(f"Circular dependency detected: {from_domain} -> {to_domain}.")
|
super().__init__(f"Circular dependency detected: {from_domain} -> {to_domain}.")
|
||||||
self.from_domain = from_domain
|
self.from_domain = from_domain
|
||||||
@ -1132,43 +1130,40 @@ def bind_hass(func: _CallableT) -> _CallableT:
|
|||||||
|
|
||||||
async def _async_component_dependencies(
|
async def _async_component_dependencies(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
start_domain: str,
|
|
||||||
integration: Integration,
|
integration: Integration,
|
||||||
loaded: set[str],
|
|
||||||
loading: set[str],
|
|
||||||
) -> set[str]:
|
) -> set[str]:
|
||||||
"""Recursive function to get component dependencies.
|
"""Get component dependencies."""
|
||||||
|
loading = set()
|
||||||
|
loaded = set()
|
||||||
|
|
||||||
Async friendly.
|
async def component_dependencies_impl(integration: Integration) -> None:
|
||||||
"""
|
"""Recursively get component dependencies."""
|
||||||
domain = integration.domain
|
domain = integration.domain
|
||||||
loading.add(domain)
|
loading.add(domain)
|
||||||
|
|
||||||
for dependency_domain in integration.dependencies:
|
for dependency_domain in integration.dependencies:
|
||||||
# Check not already loaded
|
dep_integration = await async_get_integration(hass, dependency_domain)
|
||||||
|
|
||||||
|
# If we are already loading it, we have a circular dependency.
|
||||||
|
# We have to check it here to make sure that every integration that
|
||||||
|
# depends on us, does not appear in our own after_dependencies.
|
||||||
|
if conflict := loading.intersection(dep_integration.after_dependencies):
|
||||||
|
raise CircularDependency(conflict, dependency_domain)
|
||||||
|
|
||||||
|
# If we have already loaded it, no point doing it again.
|
||||||
if dependency_domain in loaded:
|
if dependency_domain in loaded:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# If we are already loading it, we have a circular dependency.
|
# If we are already loading it, we have a circular dependency.
|
||||||
if dependency_domain in loading:
|
if dependency_domain in loading:
|
||||||
raise CircularDependency(domain, dependency_domain)
|
raise CircularDependency(dependency_domain, domain)
|
||||||
|
|
||||||
loaded.add(dependency_domain)
|
await component_dependencies_impl(dep_integration)
|
||||||
|
|
||||||
dep_integration = await async_get_integration(hass, dependency_domain)
|
|
||||||
|
|
||||||
if start_domain in dep_integration.after_dependencies:
|
|
||||||
raise CircularDependency(start_domain, dependency_domain)
|
|
||||||
|
|
||||||
if dep_integration.dependencies:
|
|
||||||
dep_loaded = await _async_component_dependencies(
|
|
||||||
hass, start_domain, dep_integration, loaded, loading
|
|
||||||
)
|
|
||||||
|
|
||||||
loaded.update(dep_loaded)
|
|
||||||
|
|
||||||
loaded.add(domain)
|
|
||||||
loading.remove(domain)
|
loading.remove(domain)
|
||||||
|
loaded.add(domain)
|
||||||
|
|
||||||
|
await component_dependencies_impl(integration)
|
||||||
|
|
||||||
return loaded
|
return loaded
|
||||||
|
|
||||||
|
@ -11,35 +11,46 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from .common import MockModule, async_get_persistent_notifications, mock_integration
|
from .common import MockModule, async_get_persistent_notifications, mock_integration
|
||||||
|
|
||||||
|
|
||||||
async def test_component_dependencies(hass: HomeAssistant) -> None:
|
async def test_circular_component_dependencies(hass: HomeAssistant) -> None:
|
||||||
"""Test if we can get the proper load order of components."""
|
"""Test if we can detect circular dependencies of components."""
|
||||||
mock_integration(hass, MockModule("mod1"))
|
mock_integration(hass, MockModule("mod1"))
|
||||||
mock_integration(hass, MockModule("mod2", ["mod1"]))
|
mock_integration(hass, MockModule("mod2", ["mod1"]))
|
||||||
mod_3 = mock_integration(hass, MockModule("mod3", ["mod2"]))
|
mock_integration(hass, MockModule("mod3", ["mod1"]))
|
||||||
|
mod_4 = mock_integration(hass, MockModule("mod4", ["mod2", "mod3"]))
|
||||||
|
|
||||||
assert {"mod1", "mod2", "mod3"} == await loader._async_component_dependencies(
|
deps = await loader._async_component_dependencies(hass, mod_4)
|
||||||
hass, "mod_3", mod_3, set(), set()
|
assert deps == {"mod1", "mod2", "mod3", "mod4"}
|
||||||
)
|
|
||||||
|
|
||||||
# Create circular dependency
|
# Create a circular dependency
|
||||||
|
mock_integration(hass, MockModule("mod1", ["mod4"]))
|
||||||
|
with pytest.raises(loader.CircularDependency):
|
||||||
|
await loader._async_component_dependencies(hass, mod_4)
|
||||||
|
|
||||||
|
# Create a different circular dependency
|
||||||
mock_integration(hass, MockModule("mod1", ["mod3"]))
|
mock_integration(hass, MockModule("mod1", ["mod3"]))
|
||||||
|
|
||||||
with pytest.raises(loader.CircularDependency):
|
with pytest.raises(loader.CircularDependency):
|
||||||
await loader._async_component_dependencies(hass, "mod_3", mod_3, set(), set())
|
await loader._async_component_dependencies(hass, mod_4)
|
||||||
|
|
||||||
# Depend on non-existing component
|
# Create a circular after_dependency
|
||||||
mod_1 = mock_integration(hass, MockModule("mod1", ["nonexisting"]))
|
mock_integration(
|
||||||
|
hass, MockModule("mod1", partial_manifest={"after_dependencies": ["mod4"]})
|
||||||
with pytest.raises(loader.IntegrationNotFound):
|
|
||||||
await loader._async_component_dependencies(hass, "mod_1", mod_1, set(), set())
|
|
||||||
|
|
||||||
# Having an after dependency 2 deps down that is circular
|
|
||||||
mod_1 = mock_integration(
|
|
||||||
hass, MockModule("mod1", partial_manifest={"after_dependencies": ["mod_3"]})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(loader.CircularDependency):
|
with pytest.raises(loader.CircularDependency):
|
||||||
await loader._async_component_dependencies(hass, "mod_3", mod_3, set(), set())
|
await loader._async_component_dependencies(hass, mod_4)
|
||||||
|
|
||||||
|
# Create a different circular after_dependency
|
||||||
|
mock_integration(
|
||||||
|
hass, MockModule("mod1", partial_manifest={"after_dependencies": ["mod3"]})
|
||||||
|
)
|
||||||
|
with pytest.raises(loader.CircularDependency):
|
||||||
|
await loader._async_component_dependencies(hass, mod_4)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_nonexistent_component_dependencies(hass: HomeAssistant) -> None:
|
||||||
|
"""Test if we can detect nonexistent dependencies of components."""
|
||||||
|
mod_1 = mock_integration(hass, MockModule("mod1", ["nonexistent"]))
|
||||||
|
with pytest.raises(loader.IntegrationNotFound):
|
||||||
|
await loader._async_component_dependencies(hass, mod_1)
|
||||||
|
|
||||||
|
|
||||||
def test_component_loader(hass: HomeAssistant) -> None:
|
def test_component_loader(hass: HomeAssistant) -> None:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user