88import pstats
99import subprocess
1010import sys
11+ import threading
1112import traceback
1213from collections import OrderedDict
1314from collections .abc import AsyncGenerator , Iterable
@@ -355,17 +356,25 @@ async def start_app(self, app_name: str):
355356 Args:
356357 app (str): Name of the app to start
357358 """
358- if self .app_config [app_name ].disable :
359- self .logger .debug (f"Skip starting disabled app: '{ app_name } '" )
360- return
359+ match self .app_config .root .get (app_name ):
360+ case AppConfig () as app_cfg :
361+ if app_cfg .disable :
362+ self .logger .debug (f"Skip starting disabled app: '{ app_name } '" )
363+ return
364+ case GlobalModule ():
365+ self .logger .warning ('Global modules cannot be started' )
366+ return
367+ case None :
368+ self .logger .error ('App %s not found in app_config' , app_name )
369+ return
361370
362371 # first we check if running already
363372 if self .is_app_running (app_name ):
364373 self .logger .warning (f"Cannot start app { app_name } , as it is already running" )
365374 return
366375
367376 # assert dependencies
368- dependencies = self . app_config . root [ app_name ] .dependencies
377+ dependencies = app_cfg .dependencies
369378 for dep_name in dependencies :
370379 rel_path = self .app_rel_path (app_name )
371380 exc_args = (
@@ -374,19 +383,27 @@ async def start_app(self, app_name: str):
374383 dep_name ,
375384 dependencies
376385 )
377- if (dep_cfg := self .app_config .root .get (dep_name )):
378- match dep_cfg :
379- case AppConfig ():
380- # There is a valid app configuration for this dependency
381- if not (obj := self .objects .get (dep_name )) or not obj .running :
382- # If the object isn't in the self.objects dict or it's there, but not running
386+ match self .app_config .root .get (dep_name ):
387+ case AppConfig ():
388+ # There is a valid app configuration for this dependency
389+ match self .objects .get (dep_name ):
390+ case ManagedObject (type = "app" ) as obj :
391+ # There is an app being managed that matches the dependency
392+ if not obj .running :
393+ # But it's not running, so raise an exception
394+ raise ade .DependencyNotRunning (* exc_args )
395+ case _:
396+ # TODO: make this a different exception
383397 raise ade .DependencyNotRunning (* exc_args )
384- case GlobalModule ():
385- module = dep_cfg .module_name
386- if module not in sys .modules :
387- raise ade .GlobalNotLoaded (* exc_args )
388- else :
389- raise ade .AppDependencyError (* exc_args )
398+ case GlobalModule () as dep_cfg :
399+ # The dependency is a legacy global module
400+ module = dep_cfg .module_name
401+ if module not in sys .modules :
402+ # The module hasn't been loaded, so raise an exception
403+ raise ade .GlobalNotLoaded (* exc_args )
404+ case _:
405+ # There was no valid configuration for the dependency
406+ raise ade .AppDependencyError (* exc_args )
390407
391408 try :
392409 await self .initialize_app (app_name )
@@ -542,10 +559,10 @@ async def terminate_sequence(self, name: str) -> bool:
542559
543560 return True
544561
545- async def read_all (self , config_files : Iterable [Path ] = None ) -> AllAppConfig :
562+ async def read_all (self , config_files : Iterable [Path ]) -> AllAppConfig :
546563 config_files = config_files or self .dependency_manager .config_files
547564
548- async def config_model_factory () -> AsyncGenerator [AllAppConfig , None , None ]:
565+ async def config_model_factory () -> AsyncGenerator [AllAppConfig , None ]:
549566 """Creates a generator that sets the config_path of app configs"""
550567 for path in config_files :
551568 @ade .wrap_async (self .error , self .AD .app_dir , "Reading user apps" )
@@ -592,7 +609,8 @@ def update(d1: dict, d2: dict) -> dict:
592609
593610 async def check_app_config_files (self , update_actions : UpdateActions ):
594611 """Updates self.mtimes_config and self.app_config"""
595- 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 ()
596614 self .dependency_manager .app_deps .update (files )
597615
598616 # If there were config file changes
@@ -670,6 +688,7 @@ def read_config_file(self, file: Path) -> AllAppConfig:
670688
671689 This function is primarily used by the create/edit/remove app methods that write yaml files.
672690 """
691+ assert threading .current_thread ().name .startswith ("ThreadPool" )
673692 raw_cfg = utils .read_config_file (file , app_config = True )
674693 if not bool (raw_cfg ):
675694 self .logger .warning (
@@ -833,7 +852,7 @@ def _process_import_paths(self):
833852 case 'default' | 'expert' | None :
834853 # Get unique set of the absolute paths of all the subdirectories containing python files
835854 python_file_parents = set (
836- f .parent .resolve () for f in Path ( self .AD . app_dir ). rglob ( "*.py" )
855+ f .parent .resolve () for f in self .get_python_files ( )
837856 )
838857
839858 # Filter out any that have __init__.py files in them
@@ -891,43 +910,65 @@ async def _init_dep_manager(self):
891910 async def safe_dep_create (self : "AppManagement" ):
892911 try :
893912 self .dependency_manager = DependencyManager (
894- python_files = await self .get_python_files (),
895- 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 ()
896915 )
897916 self .config_filecheck .mtimes = {}
898917 self .python_filecheck .mtimes = {}
899918 except ValidationError as e :
900- raise ade .BadAppConfigFile ( "Error creating dependency manager" ) from e
919+ raise ade .DependencyManagerError ( "Failed to create dependency manager" ) from e
901920 except ade .AppDaemonException as e :
902921 raise e
903922
904923 await safe_dep_create (self )
905924
906- @utils .executor_decorator
907- def get_python_files (self ) -> Iterable [Path ]:
908- """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" )
909931 return set (
910- f
911- for f in self .AD .app_dir .resolve (). rglob ( "*.py" )
912- if f . parent . name not in self . AD . exclude_dirs # apply exclude_dirs
913- and "." not in f . parent . name # also excludes *.egg-info folders
914- 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+ )
915937 )
916938
917939 @utils .executor_decorator
918- def get_app_config_files (self ) -> Iterable [Path ]:
919- """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" )
920953 return set (
921- f
922- for f in self .AD .app_dir .resolve (). rglob ( f"* { self . ext } " )
923- if f . parent . name not in self .AD . exclude_dirs # apply exclude_dirs
924- and "." not in f . stem
925- 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+ )
926959 )
927960
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+
928969 async def check_app_python_files (self , update_actions : UpdateActions ):
929970 """Checks the python files in the app directory. Part of self.check_app_updates sequence"""
930- files = await self .get_python_files ()
971+ files = await self .get_python_files_async ()
931972 self .dependency_manager .update_python_files (files )
932973
933974 # We only need to init the modules necessary for the new apps, not reloaded ones
0 commit comments