1717import os
1818import signal
1919import sys
20- from collections .abc import Callable
20+ from collections .abc import Callable , Generator
2121from contextlib import ExitStack , contextmanager
2222from logging import Logger
2323from pathlib import Path
@@ -271,8 +271,9 @@ def parse_config(args: argparse.Namespace, stop_function: Callable) -> MainConfi
271271
272272
273273class ADMain :
274- """
275- Class to encapsulate all main() functionality.
274+ """Main application class for AppDaemon, which contains the parsed CLI arguments, top-level config model, and the async event loop.
275+
276+ When this class is instantiated, it creates a :py:class:`~appdaemon.dependency_manager.DependencyManager` from the app directory. This causes
276277 """
277278
278279 AD : AppDaemon
@@ -286,13 +287,13 @@ class ADMain:
286287 _cleanup_stack : ExitStack
287288
288289 model : MainConfig
290+ """Pydantic model of the top-level object for the appdaemon.yaml file."""
289291 args : argparse .Namespace
290292
291293 stop_time : float = 0.0
292294 """Stores the value of perf_counter() when self.stop is first called."""
293295
294- def __init__ (self , args : argparse .Namespace ):
295- """Constructor."""
296+ def __init__ (self , args : argparse .Namespace ) -> None :
296297 self .args = args
297298 self .http_object = None
298299 self ._cleanup_stack = ExitStack ()
@@ -302,7 +303,7 @@ def __init__(self, args: argparse.Namespace):
302303 self .setup_logging ()
303304 utils .deprecation_warnings (self .model .appdaemon , self .logger )
304305
305- # # Create the dependency manager here so that all the initial file reading happens in here
306+ # Create the dependency manager here so that all the initial file reading happens in here
306307 self .dep_manager = DependencyManager .from_app_directory (
307308 self .model .appdaemon .app_dir ,
308309 exclude = self .model .appdaemon .exclude_dirs ,
@@ -325,10 +326,10 @@ def __enter__(self):
325326 pidfile_path = Path (self .args .pidfile ).resolve ()
326327 self .logger .info ("Using pidfile: %s" , pidfile_path )
327328 pid_file = pid .PidFile (pidfile_path .name , pidfile_path .parent )
328- self .enter_context (pid_file )
329+ self ._cleanup_stack . enter_context (pid_file )
329330
330- self .enter_context (self .loop_context ())
331- self .enter_context (self .signal_handlers (self .loop ))
331+ self ._cleanup_stack . enter_context (self .loop_context ())
332+ self ._cleanup_stack . enter_context (self .signal_handlers (self .loop ))
332333 return self
333334 except Exception as e :
334335 ade .user_exception_block (self .logger , e , self .model .appdaemon .app_dir , header = "ADMain __enter__" )
@@ -342,10 +343,6 @@ def add_cleanup(self, cleanup_func, *args, **kwargs):
342343 """Add a cleanup function to be called on exit."""
343344 self ._cleanup_stack .callback (cleanup_func , * args , ** kwargs )
344345
345- def enter_context (self , context_manager ):
346- """Enter a context manager and ensure it's cleaned up on exit."""
347- return self ._cleanup_stack .enter_context (context_manager )
348-
349346 def handle_sig (self , signum : int ):
350347 """Function to handle signals.
351348
@@ -366,13 +363,13 @@ def handle_sig(self, signum: int):
366363 case (signal .SIGINT | signal .SIGTERM ) as sig :
367364 self .logger .info (f"Received signal: { signal .Signals (sig ).name } " )
368365 self .stop ()
369- # case signal.SIGWINCH:
370- # ... # disregard window changes
371- # case _:
372- # self.logger.error(f'Unhandled signal: {signal.Signals(signum).name}')
373366
374367 @contextmanager
375- def loop_context (self ):
368+ def loop_context (self ) -> Generator [asyncio .AbstractEventLoop ]:
369+ """Context manager that creates a new async event loop and cleans it up afterwards.
370+
371+ Includes the logic to install uvloop if it's enabled.
372+ """
376373 # uvloop needs to be installed outside of self.run_context
377374 if self .model .appdaemon .uvloop and uvloop is not None :
378375 uvloop .install ()
@@ -410,15 +407,18 @@ def signal_handlers(self, loop: asyncio.AbstractEventLoop):
410407 pass
411408
412409 def stop (self ):
413- """Called by the signal handler to shut AD down ."""
410+ """Stop AppDaemon and stop the event loop afterwards ."""
414411 self .stop_time = perf_counter ()
415412 task = self .loop .create_task (self .AD .stop ())
416413 task .add_done_callback (lambda _ : self .loop .stop ())
417414
418415 def run (self ) -> None :
419- """Start AppDaemon up after initial argument parsing."""
420- self .enter_context (self .startup_text ())
421- self .enter_context (self .run_context (self .loop ))
416+ """Start AppDaemon up after initial argument parsing.
417+
418+ This uses :py:meth:`~asyncio.loop.run_forever` on the event loop to run it indefinitely.
419+ """
420+ self ._cleanup_stack .enter_context (self .startup_text ())
421+ self ._cleanup_stack .enter_context (self .run_context (self .loop ))
422422 self .AD .start ()
423423 self .logger .debug ("Running async event loop forever" )
424424 self .loop .run_forever ()
@@ -495,17 +495,22 @@ def startup_text(self):
495495 self .logger .info ("AppDaemon main() stopped gracefully in %s" , utils .format_timedelta (stop_duration ))
496496
497497
498- def main ():
498+ def main () -> None :
499+ """Top-level entrypoint for AppDaemon
500+
501+ Parses the CLI arguments, configures logging, and runs the AppDaemon.
502+ """
499503 args = parse_arguments ()
500504
505+ CLI_LOG_CFG = PRE_LOGGING .copy ()
506+
501507 if args .debug is not None :
502- CLI_LOG_CFG = PRE_LOGGING .copy ()
503508 CLI_LOG_CFG ["root" ]["level" ] = args .debug
504- logging .config .dictConfig (CLI_LOG_CFG )
505509 logger .debug ("Configured logging level from command line argument" )
506510
511+ logging .config .dictConfig (CLI_LOG_CFG )
512+
507513 with ADMain (args ) as admain :
508- # raise ade.StartupAbortedException()
509514 admain .run ()
510515
511516
0 commit comments