From 3ac72d3b6184a2da85a516fd1ff3ab0efc59063d Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:29:57 -0600 Subject: [PATCH 1/2] better support for mixing yaml and toml apps --- appdaemon/__main__.py | 1 - appdaemon/app_management.py | 12 +++++------- appdaemon/dependency_manager.py | 23 ++++++++++++++--------- appdaemon/utils.py | 5 +++-- tests/conftest.py | 5 +++-- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/appdaemon/__main__.py b/appdaemon/__main__.py index c7b38cf63..b192b369b 100644 --- a/appdaemon/__main__.py +++ b/appdaemon/__main__.py @@ -296,7 +296,6 @@ def __init__(self, args: argparse.Namespace) -> None: self.dep_manager = DependencyManager.from_app_directory( self.model.appdaemon.app_dir, exclude=self.model.appdaemon.exclude_dirs, - config_suffix=self.model.appdaemon.ext, ) except Exception as e: diff --git a/appdaemon/app_management.py b/appdaemon/app_management.py index fb87c5d1f..312211559 100644 --- a/appdaemon/app_management.py +++ b/appdaemon/app_management.py @@ -21,7 +21,6 @@ from pydantic import ValidationError - from appdaemon.dependency import DependencyResolutionFail, find_all_dependents, get_full_module_name from appdaemon.dependency_manager import DependencyManager from appdaemon.models.config import AllAppConfig, AppConfig, GlobalModule @@ -33,9 +32,9 @@ from .models.internal.app_management import LoadingActions, ManagedObject, UpdateActions, UpdateMode if TYPE_CHECKING: - from .appdaemon import AppDaemon - from .adbase import ADBase from .adapi import ADAPI + from .adbase import ADBase + from .appdaemon import AppDaemon from .plugin_management import PluginBase T = TypeVar("T") @@ -87,7 +86,6 @@ class AppManagement: def __init__(self, ad: "AppDaemon"): self.AD = ad - self.ext = self.AD.config.ext self.logger = ad.logging.get_child(self.name) self.error = ad.logging.get_error() self.diag = ad.logging.get_diag() @@ -1047,8 +1045,8 @@ def get_app_config_files(self) -> set[Path]: return set( utils.recursive_get_files( base=self.AD.app_dir.resolve(), - suffix=self.ext, - exclude=set(self.AD.exclude_dirs), + suffix={".yaml", ".toml"}, + exclude=set(self.AD.exclude_dirs) | {"ruff.toml", "pyproject.toml", "secrets.yaml"}, ) ) @@ -1323,7 +1321,7 @@ def create_app(self, app: str = None, **kwargs): return False app_directory: Path = self.AD.app_dir / kwargs.pop("app_dir", "ad_apps") - app_file: Path = app_directory / kwargs.pop("app_file", f"{app}{self.ext}") + app_file: Path = app_directory / kwargs.pop("app_file", f"{app}{self.AD.config.ext}") app_directory = app_file.parent # in case the given app_file is multi level try: diff --git a/appdaemon/dependency_manager.py b/appdaemon/dependency_manager.py index 134ea323f..9cc8540de 100644 --- a/appdaemon/dependency_manager.py +++ b/appdaemon/dependency_manager.py @@ -1,7 +1,6 @@ from abc import ABC from copy import deepcopy from dataclasses import InitVar, dataclass, field -from functools import partial from pathlib import Path from typing import Iterable @@ -16,7 +15,7 @@ class Dependencies(ABC): """Wraps an instance of ``FileCheck`` with a corresponding set of dependency graphs.""" files: FileCheck = field(repr=False) - ext: str = field(init=False) # this has to be defined by the children classes + ext: str | set[str] = field(init=False) # this has to be defined by the children classes dep_graph: dict[str, set[str]] = field(init=False) rev_graph: dict[str, set[str]] = field(init=False) bad_files: set[tuple[Path, float]] = field(default_factory=set, init=False) @@ -39,7 +38,13 @@ def refresh_dep_graph(self): @classmethod def from_path(cls, path: Path): - return cls.from_paths(path.rglob(f"*{cls.ext}")) + return cls.from_paths( + utils.recursive_get_files( + base=path, + suffix={".yaml", ".toml"}, + exclude={"ruff.toml", "pyproject.toml", "secrets.yaml"}, + ) + ) @classmethod def from_paths(cls, paths: Iterable[Path]): @@ -90,7 +95,6 @@ def modules_to_delete(self) -> list[str]: @dataclass class AppDeps(Dependencies): app_config: AllAppConfig = field(init=False, repr=False) - ext: str = ".yaml" def __post_init__(self): self.app_config = AllAppConfig.from_config_files(self.files) @@ -157,7 +161,6 @@ def from_app_directory( cls, app_dir: Path, exclude: str | Iterable[str] | None = None, - config_suffix: str = ".yaml", ) -> "DependencyManager": """Creates a new instance of the dependency manager from the given app directory""" match exclude: @@ -168,12 +171,14 @@ def from_app_directory( case _: exclude_set = set(exclude) - get_files = partial(utils.recursive_get_files, base=app_dir, exclude=exclude_set) return cls( - # python_files=get_files(suffix=".py"), python_files=list(), - config_files=get_files(suffix=config_suffix) - ) # fmt: skip + config_files=utils.recursive_get_files( + base=app_dir, + suffix={".yaml", ".toml"}, + exclude=exclude_set, + ) + ) @property def app_config_files(self) -> set[Path]: diff --git a/appdaemon/utils.py b/appdaemon/utils.py index c8c46c67a..7d495dc4b 100644 --- a/appdaemon/utils.py +++ b/appdaemon/utils.py @@ -1217,7 +1217,7 @@ def deprecation_warnings(model: BaseModel, logger: Logger): deprecation_warnings(attr, logger) -def recursive_get_files(base: Path, suffix: str, exclude: set[str] | None = None) -> Generator[Path, None, None]: +def recursive_get_files(base: Path, suffix: str | set[str], exclude: set[str] | None = None) -> Generator[Path, None, None]: """Recursively generate file paths. Args: @@ -1228,11 +1228,12 @@ def recursive_get_files(base: Path, suffix: str, exclude: set[str] | None = None Yields: Path objects to files that have the matching extension and are readable. """ + suffix = {suffix} if isinstance(suffix, str) else suffix exclude = set() if exclude is None else exclude for item in base.iterdir(): if item.name.startswith(".") or (exclude is None or item.name in exclude): continue - elif item.is_file() and item.suffix == suffix and os.access(item, os.R_OK): + elif item.is_file() and item.suffix in suffix and os.access(item, os.R_OK): yield item elif item.is_dir() and os.access(item, os.R_OK): yield from recursive_get_files(item, suffix, exclude) diff --git a/tests/conftest.py b/tests/conftest.py index 2a5938f52..9f546caa2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,13 +6,14 @@ import pytest import pytest_asyncio -from appdaemon import AppDaemon from appdaemon.dependency_manager import DependencyManager from appdaemon.logging import Logging from appdaemon.models.config.app import AppConfig from appdaemon.models.config.appdaemon import AppDaemonConfig from appdaemon.utils import format_timedelta, recursive_get_files +from appdaemon import AppDaemon + logger = logging.getLogger("AppDaemon._test") @@ -51,7 +52,7 @@ async def ad(ad_obj: AppDaemon, running_loop: asyncio.BaseEventLoop) -> AsyncGen # logger.info(f"Passed loop: {hex(id(running_loop))}") assert running_loop == asyncio.get_running_loop(), "The running loop should match the one passed in" ad = ad_obj - config_files = list(recursive_get_files(base=ad.app_dir, suffix=ad.config.ext)) + config_files = list(recursive_get_files(base=ad.app_dir, suffix={'.yaml', '.toml'})) ad.app_management.dependency_manager = DependencyManager(python_files=list(), config_files=config_files) for cfg in ad.app_management.app_config.root.values(): From cebcf687b7c2741f07fdd4b5aa3449082f38eae3 Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:38:28 -0600 Subject: [PATCH 2/2] lint fix --- tests/conftest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9f546caa2..5decf26fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,14 +6,13 @@ import pytest import pytest_asyncio +from appdaemon import AppDaemon from appdaemon.dependency_manager import DependencyManager from appdaemon.logging import Logging from appdaemon.models.config.app import AppConfig from appdaemon.models.config.appdaemon import AppDaemonConfig from appdaemon.utils import format_timedelta, recursive_get_files -from appdaemon import AppDaemon - logger = logging.getLogger("AppDaemon._test")