Skip to content

Commit 69a36a7

Browse files
committed
reworked file iteration and discovery
1 parent 931c54e commit 69a36a7

File tree

4 files changed

+78
-25
lines changed

4 files changed

+78
-25
lines changed

appdaemon/app_management.py

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pstats
99
import subprocess
1010
import sys
11+
import threading
1112
import traceback
1213
from collections import OrderedDict
1314
from collections.abc import AsyncGenerator, Iterable
@@ -558,10 +559,10 @@ async def terminate_sequence(self, name: str) -> bool:
558559

559560
return True
560561

561-
async def read_all(self, config_files: Iterable[Path] = None) -> AllAppConfig:
562+
async def read_all(self, config_files: Iterable[Path]) -> AllAppConfig:
562563
config_files = config_files or self.dependency_manager.config_files
563564

564-
async def config_model_factory() -> AsyncGenerator[AllAppConfig, None, None]:
565+
async def config_model_factory() -> AsyncGenerator[AllAppConfig, None]:
565566
"""Creates a generator that sets the config_path of app configs"""
566567
for path in config_files:
567568
@ade.wrap_async(self.error, self.AD.app_dir, "Reading user apps")
@@ -608,7 +609,8 @@ def update(d1: dict, d2: dict) -> dict:
608609

609610
async def check_app_config_files(self, update_actions: UpdateActions):
610611
"""Updates self.mtimes_config and self.app_config"""
611-
files = await self.get_app_config_files()
612+
# get_files_in_other_thread = utils.executor_decorator(self.get_app_config_files)
613+
files = await self.get_app_config_files_async()
612614
self.dependency_manager.app_deps.update(files)
613615

614616
# If there were config file changes
@@ -686,6 +688,7 @@ def read_config_file(self, file: Path) -> AllAppConfig:
686688
687689
This function is primarily used by the create/edit/remove app methods that write yaml files.
688690
"""
691+
assert threading.current_thread().name.startswith("ThreadPool")
689692
raw_cfg = utils.read_config_file(file, app_config=True)
690693
if not bool(raw_cfg):
691694
self.logger.warning(
@@ -849,7 +852,7 @@ def _process_import_paths(self):
849852
case 'default' | 'expert' | None:
850853
# Get unique set of the absolute paths of all the subdirectories containing python files
851854
python_file_parents = set(
852-
f.parent.resolve() for f in Path(self.AD.app_dir).rglob("*.py")
855+
f.parent.resolve() for f in self.get_python_files()
853856
)
854857

855858
# Filter out any that have __init__.py files in them
@@ -907,43 +910,65 @@ async def _init_dep_manager(self):
907910
async def safe_dep_create(self: "AppManagement"):
908911
try:
909912
self.dependency_manager = DependencyManager(
910-
python_files=await self.get_python_files(),
911-
config_files=await self.get_app_config_files()
913+
python_files=await self.get_python_files_async(),
914+
config_files=await self.get_app_config_files_async()
912915
)
913916
self.config_filecheck.mtimes = {}
914917
self.python_filecheck.mtimes = {}
915918
except ValidationError as e:
916-
raise ade.BadAppConfigFile("Error creating dependency manager") from e
919+
raise ade.DependencyManagerError("Failed to create dependency manager") from e
917920
except ade.AppDaemonException as e:
918921
raise e
919922

920923
await safe_dep_create(self)
921924

922-
@utils.executor_decorator
923-
def get_python_files(self) -> Iterable[Path]:
924-
"""Iterates through ``*.py`` in the app directory. Excludes directory names defined in exclude_dirs and with a "." character. Also excludes files that aren't readable."""
925+
def get_python_files(self) -> set[Path]:
926+
"""Get a set of valid Python files in the app directory.
927+
928+
Valid files are ones that are readable, not inside an excluded directory, and not starting with a "." character.
929+
"""
930+
assert threading.current_thread().name.startswith("ThreadPool")
925931
return set(
926-
f
927-
for f in self.AD.app_dir.resolve().rglob("*.py")
928-
if f.parent.name not in self.AD.exclude_dirs # apply exclude_dirs
929-
and "." not in f.parent.name # also excludes *.egg-info folders
930-
and os.access(f, os.R_OK) # skip unreadable files
932+
utils.recursive_get_files(
933+
base=self.AD.app_dir.resolve(),
934+
suffix=".py",
935+
exclude=set(self.AD.exclude_dirs),
936+
)
931937
)
932938

933939
@utils.executor_decorator
934-
def get_app_config_files(self) -> Iterable[Path]:
935-
"""Iterates through config files in the config directory. Excludes directory names defined in exclude_dirs and files with a "." character. Also excludes files that aren't readable."""
940+
def get_python_files_async(self) -> set[Path]:
941+
"""Get a set of valid app config files in the app directory.
942+
943+
Valid files are ones that are readable, not inside an excluded directory, and not starting with a "." character.
944+
"""
945+
return self.get_python_files()
946+
947+
def get_app_config_files(self) -> set[Path]:
948+
"""Get a set of valid app fonfig files in the app directory.
949+
950+
Valid files are ones that are readable, not inside an excluded directory, and not starting with a "." character.
951+
"""
952+
assert threading.current_thread().name.startswith("ThreadPool")
936953
return set(
937-
f
938-
for f in self.AD.app_dir.resolve().rglob(f"*{self.ext}")
939-
if f.parent.name not in self.AD.exclude_dirs # apply exclude_dirs
940-
and "." not in f.stem
941-
and os.access(f, os.R_OK) # skip unreadable files
954+
utils.recursive_get_files(
955+
base=self.AD.app_dir.resolve(),
956+
suffix=self.ext,
957+
exclude=set(self.AD.exclude_dirs),
958+
)
942959
)
943960

961+
@utils.executor_decorator
962+
def get_app_config_files_async(self) -> set[Path]:
963+
"""Get a set of valid app config files in the app directory.
964+
965+
Valid files are ones that are readable, not inside an excluded directory, and not starting with a "." character.
966+
"""
967+
return self.get_app_config_files()
968+
944969
async def check_app_python_files(self, update_actions: UpdateActions):
945970
"""Checks the python files in the app directory. Part of self.check_app_updates sequence"""
946-
files = await self.get_python_files()
971+
files = await self.get_python_files_async()
947972
self.dependency_manager.update_python_files(files)
948973

949974
# We only need to init the modules necessary for the new apps, not reloaded ones

appdaemon/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,14 @@ def __str__(self):
385385
return f"Class '{self.class_name}' takes the wrong number of arguments. Check the inheritance"
386386

387387

388+
@dataclass
389+
class DependencyManagerError(AppDaemonException):
390+
msg: str
391+
392+
def __str__(self) -> str:
393+
return self.msg
394+
395+
388396
@dataclass
389397
class AppDependencyError(AppDaemonException):
390398
app_name: str

appdaemon/threads.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ async def create_initial_threads(self):
171171
else:
172172
# Force a config check here so we have an accurate activate app count
173173
self.AD.app_management.logger.debug("Reading app config files to determine how many threads to make")
174-
cfg_paths = await self.AD.app_management.get_app_config_files()
174+
cfg_paths = await self.AD.app_management.get_app_config_files_async()
175175
if not cfg_paths:
176176
self.logger.warning(f"No apps found in {self.AD.app_dir}. This is probably a mistake")
177177
self.total_threads = 10

appdaemon/utils.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import threading
1717
import time
1818
import traceback
19-
from collections.abc import Awaitable, Iterable
19+
from collections.abc import Awaitable, Generator, Iterable
2020
from datetime import timedelta, tzinfo
2121
from functools import wraps
2222
from logging import Logger
@@ -1120,3 +1120,23 @@ def deprecation_warnings(model: BaseModel, logger: Logger):
11201120
deprecation_warnings(val, logger)
11211121
case BaseModel():
11221122
deprecation_warnings(attr, logger)
1123+
1124+
1125+
def recursive_get_files(base: Path, suffix: str, exclude: set[str] | None = None) -> Generator[Path, None, None]:
1126+
"""Recursively generate file paths.
1127+
1128+
Args:
1129+
base (Path): The base directory to start searching from.
1130+
suffix (str): The file extension to filter by.
1131+
exclude (set[str]): A set of directory names to exclude from the search.
1132+
1133+
Yields:
1134+
Path objects to files that have the matching extension and are readable.
1135+
"""
1136+
for item in base.iterdir():
1137+
if item.name.startswith(".") or (exclude is None or item.name in exclude):
1138+
continue
1139+
elif item.is_file() and item.suffix == suffix and os.access(item, os.R_OK):
1140+
yield item
1141+
elif item.is_dir() and os.access(item, os.R_OK):
1142+
yield from recursive_get_files(item, suffix, exclude)

0 commit comments

Comments
 (0)