diff --git a/appdaemon/app_management.py b/appdaemon/app_management.py index 2efd27e50..c45a23f18 100644 --- a/appdaemon/app_management.py +++ b/appdaemon/app_management.py @@ -720,7 +720,7 @@ async def check_app_config_files(self, update_actions: UpdateActions): self.app_config.root.update(freshly_read_cfg.root) if update_actions.apps.init_set: - # If there are any new apps, the dependency graph needs to be updated + # If there are any new/modified apps, the dependency graph needs to be updated self.dependency_manager.app_deps.refresh_dep_graph() if self.AD.threading.pin_apps: @@ -921,8 +921,7 @@ def _process_import_paths(self): package_parents = set(p.parent for p in top_packages_dirs) # Combine import directories. Having the list sorted will prioritize parent folders over children during import - import_dirs = sorted( - module_parents | package_parents, reverse=True) + import_dirs = sorted(module_parents | package_parents, reverse=True) for path in import_dirs: self.add_to_import_path(path) @@ -1088,8 +1087,7 @@ async def safe_create(self: "AppManagement"): await self.AD.threading.calculate_pin_threads() # Need to recalculate start order in case creating the app object fails - start_order = update_actions.apps.start_sort( - self.dependency_manager) + start_order = update_actions.apps.start_sort(self.dependency_manager, self.logger) for app_name in start_order: if isinstance((cfg := self.app_config.root[app_name]), AppConfig): rel_path = self.app_rel_path(app_name) diff --git a/appdaemon/dependency.py b/appdaemon/dependency.py index d3bc86661..ac53bf70e 100644 --- a/appdaemon/dependency.py +++ b/appdaemon/dependency.py @@ -233,6 +233,7 @@ def visit(node: str): if node not in visited: visit(node) if cycle_detected: - raise CircularDependency(f"Visited {visited} already and was going visit {node} again") + deps = graph[node] + raise CircularDependency(f"Visited {visited} already, but {node} depends on {deps}") return stack diff --git a/appdaemon/models/internal/app_management.py b/appdaemon/models/internal/app_management.py index b901e4b2c..823910a86 100644 --- a/appdaemon/models/internal/app_management.py +++ b/appdaemon/models/internal/app_management.py @@ -1,3 +1,4 @@ +from logging import Logger import uuid from copy import copy, deepcopy from dataclasses import dataclass, field @@ -9,6 +10,7 @@ from ...dependency_manager import DependencyManager + class UpdateMode(Enum): """Used as an argument for :meth:`AppManagement.check_app_updates` to set the mode of the check. @@ -62,7 +64,7 @@ def import_sort(self, dm: DependencyManager) -> list[str]: order = [n for n in topo_sort(dm.python_deps.dep_graph) if n in items] return order - def start_sort(self, dm: DependencyManager) -> list[str]: + def start_sort(self, dm: DependencyManager, logger: Logger = None) -> list[str]: """Finds the apps that need to be started. Uses a dependency graph to sort the internal ``init`` and ``reload`` sets together @@ -83,7 +85,14 @@ def start_sort(self, dm: DependencyManager) -> list[str]: dep_graph = deepcopy(dm.app_deps.dep_graph) for app, deps in dep_graph.items(): - deps |= priority_deps.get(app, set()) + # need to make sure the app isn't in it's own dependencies + for dep in priority_deps.get(app, set()): + sub_deps = find_all_dependents({app}, dm.app_deps.rev_graph) + if dep in sub_deps: + if logger is not None: + logger.warning(f'Applying priority will cause a circular dependency: {app} -> {dep}') + else: + deps.add(dep) order = [n for n in topo_sort(dep_graph) if n in items] return order