22Exceptions used by appdaemon
33
44"""
5+
56import asyncio
67import functools
78import inspect
1718from pathlib import Path
1819from typing import TYPE_CHECKING , Any , Type
1920
21+ from aiohttp .client_exceptions import ClientConnectorError
2022from pydantic import ValidationError
2123
2224if TYPE_CHECKING :
@@ -34,24 +36,27 @@ def get_callback_sig(funcref) -> str:
3436@dataclass
3537class AppDaemonException (Exception , ABC ):
3638 """Abstract base class for all AppDaemon exceptions to inherit from"""
37- # msg: str
3839
3940 def __post_init__ (self ):
40- if msg := getattr (self , ' msg' , None ):
41+ if msg := getattr (self , " msg" , None ):
4142 super (Exception , self ).__init__ (msg )
4243
4344
44- def exception_handler (appdaemon : "AppDaemon" , loop : asyncio .AbstractEventLoop , context : dict ):
45+ def exception_handler (appdaemon : "AppDaemon" , loop : asyncio .AbstractEventLoop , context : dict [ str , Any ] ):
4546 """Handler to attach to the main event loop as a backstop for any async exception"""
46- user_exception_block (
47- logging .getLogger ('Error' ),
48- context .get ('exception' ),
49- appdaemon .app_dir ,
50- header = 'Unhandled exception in event loop'
51- )
47+ match context :
48+ case {"exception" : Exception () as exc , "future" : asyncio .Task () as task }:
49+ user_exception_block (
50+ logging .getLogger ("Error" ),
51+ exception = exc ,
52+ app_dir = appdaemon .app_dir ,
53+ header = f"Unhandled exception in { task .get_name ()} " ,
54+ )
55+ case _:
56+ logging .getLogger ("Error" ).error (f"Unhandled exception in event loop: { context } " )
5257
5358
54- def user_exception_block (logger : Logger , exception : AppDaemonException , app_dir : Path , header : str | None = None ):
59+ def user_exception_block (logger : Logger , exception : Exception , app_dir : Path , header : str | None = None ):
5560 """Generate a user-friendly block of text for an exception.
5661
5762 Gets the whole chain of exception causes to decide what to do.
@@ -60,15 +65,15 @@ def user_exception_block(logger: Logger, exception: AppDaemonException, app_dir:
6065 spacing = 4
6166 inset = 5
6267 if header is not None :
63- header = f' { "=" * inset } { header } { "=" * (width - spacing - inset - len (header ))} '
68+ header = f" { '=' * inset } { header } { '=' * (width - spacing - inset - len (header ))} "
6469 else :
65- header = '=' * width
70+ header = "=" * width
6671 logger .error (header )
6772
6873 chain = get_exception_cause_chain (exception )
6974
7075 for i , exc in enumerate (chain ):
71- indent = ' ' * i * 2
76+ indent = " " * i * 2
7277
7378 match exc :
7479 case AssertionError ():
@@ -90,53 +95,56 @@ def user_exception_block(logger: Logger, exception: AppDaemonException, app_dir:
9095 case AppDaemonException ():
9196 for i , line in enumerate (str (exc ).splitlines ()):
9297 if i == 0 :
93- logger .error (f' { indent } { exc .__class__ .__name__ } : { line } ' )
98+ logger .error (f" { indent } { exc .__class__ .__name__ } : { line } " )
9499 else :
95- logger .error (f' { indent } { line } ' )
100+ logger .error (f" { indent } { line } " )
96101
97102 if user_line := get_user_line (exc , app_dir ):
98103 for line , filename , func_name in list (user_line )[::- 1 ]:
99- logger .error (f'{ indent } { filename } line { line } in { func_name } ' )
100- case OSError () if str (exc ).endswith ('address already in use' ):
101- logger .error (f'{ indent } { exc .__class__ .__name__ } : { exc } ' )
104+ logger .error (f"{ indent } { filename } line { line } in { func_name } " )
105+ case ClientConnectorError ():
106+ logger .error (f"{ indent } { exc .__class__ .__name__ } : { exc } " )
107+ break
108+ case OSError () if str (exc ).endswith ("address already in use" ):
109+ logger .error (f"{ indent } { exc .__class__ .__name__ } : { exc } " )
102110 case NameError () | ImportError ():
103- logger .error (f' { indent } { exc .__class__ .__name__ } : { exc } ' )
111+ logger .error (f" { indent } { exc .__class__ .__name__ } : { exc } " )
104112 if tb := traceback .extract_tb (exc .__traceback__ ):
105113 frame = tb [- 1 ]
106114 file = Path (frame .filename ).relative_to (app_dir .parent )
107- logger .error (f' { indent } line { frame .lineno } in { file .name } ' )
108- logger .error (f' { indent } { frame ._line .rstrip ()} ' )
115+ logger .error (f" { indent } line { frame .lineno } in { file .name } " )
116+ logger .error (f" { indent } { frame ._line .rstrip ()} " )
109117 error_len = frame .end_colno - frame .colno
110- logger .error (f' { indent } { " " * (frame .colno - 1 )} { "^" * error_len } ' )
118+ logger .error (f" { indent } { ' ' * (frame .colno - 1 )} { '^' * error_len } " )
111119 case SyntaxError ():
112- logger .error (f' { indent } { exc .__class__ .__name__ } : { exc } ' )
113- logger .error (f' { indent } { exc .text .rstrip ()} ' )
120+ logger .error (f" { indent } { exc .__class__ .__name__ } : { exc } " )
121+ logger .error (f" { indent } { exc .text .rstrip ()} " )
114122
115123 if exc .end_offset == 0 :
116124 error_len = len (exc .text ) - exc .offset
117125 else :
118126 error_len = exc .end_offset - exc .offset
119- logger .error (f' { indent } { " " * (exc .offset - 1 )} { "^" * error_len } ' )
127+ logger .error (f" { indent } { ' ' * (exc .offset - 1 )} { '^' * error_len } " )
120128 case _:
121- logger .error (f' { indent } { exc .__class__ .__name__ } : { exc } ' )
129+ logger .error (f" { indent } { exc .__class__ .__name__ } : { exc } " )
122130 if tb := traceback .extract_tb (exc .__traceback__ ):
123131 # filtered = (fs for fs in tb if 'appdaemon' in fs.filename)
124132 # filtered = tb
125133 # ss = traceback.StackSummary.from_list(filtered)
126134 lines = (line for fl in tb .format () for line in fl .splitlines ())
127135 for line in lines :
128- logger .error (f' { indent } { line } ' )
136+ logger .error (f" { indent } { line } " )
129137
130- logger .error ('=' * width )
138+ logger .error ("=" * width )
131139
132140
133141def unexpected_block (logger : Logger , exception : Exception ):
134- logger .error ('=' * 75 )
135- logger .error (f' Unexpected error: { exception } ' )
142+ logger .error ("=" * 75 )
143+ logger .error (f" Unexpected error: { exception } " )
136144 formatted = traceback .format_exc ()
137145 for line in formatted .splitlines ():
138146 logger .error (line )
139- logger .error ('=' * 75 )
147+ logger .error ("=" * 75 )
140148
141149
142150def get_cause_lines (chain : Iterable [Exception ]) -> dict [Exception , list [traceback .FrameSummary ]]:
@@ -172,7 +180,9 @@ async def wrapper(*args, **kwargs):
172180 user_exception_block (logger , e , app_dir , header )
173181 except Exception as e :
174182 unexpected_block (logger , e )
183+
175184 return wrapper
185+
176186 return decorator
177187
178188
@@ -186,7 +196,9 @@ def wrapper(*args, **kwargs):
186196 user_exception_block (logger , e , app_dir , header )
187197 except Exception as e :
188198 unexpected_block (logger , e )
199+
189200 return wrapper
201+
190202 return decorator
191203
192204
@@ -244,11 +256,7 @@ class ServiceException(AppDaemonException):
244256 domain_services : list [str ]
245257
246258 def __str__ (self ):
247- return (
248- f"domain '{ self .domain } ' exists in namespace '{ self .namespace } ', "
249- f"but does not contain service '{ self .service } '. "
250- f"Services that exist in { self .domain } : { ', ' .join (self .domain_services )} "
251- )
259+ return f"domain '{ self .domain } ' exists in namespace '{ self .namespace } ', but does not contain service '{ self .service } '. Services that exist in { self .domain } : { ', ' .join (self .domain_services )} "
252260
253261
254262@dataclass
@@ -263,17 +271,18 @@ def __str__(self):
263271@dataclass
264272class AppCallbackFail (AppDaemonException ):
265273 """Base class for exceptions caused by callbacks made in user apps."""
274+
266275 app_name : str
267276 funcref : functools .partial
268277
269278 def __str__ (self , base : str | None = None ):
270279 base = base or f"Callback failed for app '{ self .app_name } '"
271280
272281 if args := self .funcref .args :
273- base += f' \n args: { args } '
282+ base += f" \n args: { args } "
274283
275284 if kwargs := self .funcref .keywords :
276- base += f' \n kwargs: { json .dumps (kwargs , indent = 4 , default = str )} '
285+ base += f" \n kwargs: { json .dumps (kwargs , indent = 4 , default = str )} "
277286
278287 return base
279288
@@ -287,10 +296,10 @@ def __str__(self):
287296
288297 # Type errors are a special case where we can give some more advice about how the callback should be written
289298 if isinstance (self .__cause__ , TypeError ):
290- res += f' \n { self .__cause__ } '
291- res += ' \n State callbacks should have the following signature:'
292- res += ' \n state_callback(self, entity, attribute, old, new, **kwargs)'
293- res += ' \n See https://appdaemon.readthedocs.io/en/latest/APPGUIDE.html#state-callbacks for more information'
299+ res += f" \n { self .__cause__ } "
300+ res += " \n State callbacks should have the following signature:"
301+ res += " \n state_callback(self, entity, attribute, old, new, **kwargs)"
302+ res += " \n See https://appdaemon.readthedocs.io/en/latest/APPGUIDE.html#state-callbacks for more information"
294303
295304 return res
296305
@@ -301,8 +310,8 @@ def __str__(self):
301310 res = super ().__str__ (f"Scheduled callback failed for app '{ self .app_name } '" )
302311
303312 if isinstance (self .__cause__ , TypeError ):
304- res += f' \n Callback has signature: { get_callback_sig (self .funcref )} '
305- res += f' \n { self .__cause__ } \n '
313+ res += f" \n Callback has signature: { get_callback_sig (self .funcref )} "
314+ res += f" \n { self .__cause__ } \n "
306315 return res
307316
308317
@@ -314,10 +323,10 @@ def __str__(self):
314323 res = super ().__str__ (f"Scheduled callback failed for app '{ self .app_name } '" )
315324
316325 if isinstance (self .__cause__ , TypeError ):
317- res += f' \n { self .__cause__ } '
318- res += ' \n State callbacks should have the following signature:'
319- res += ' \n my_callback(self, event_name, data, **kwargs):'
320- res += ' \n See https://appdaemon.readthedocs.io/en/latest/APPGUIDE.html#event-callbacks for more information'
326+ res += f" \n { self .__cause__ } "
327+ res += " \n State callbacks should have the following signature:"
328+ res += " \n my_callback(self, event_name, data, **kwargs):"
329+ res += " \n See https://appdaemon.readthedocs.io/en/latest/APPGUIDE.html#event-callbacks for more information"
321330 return res
322331
323332
@@ -416,6 +425,7 @@ def __str__(self):
416425 res += f" pin_threads: { self .pin_threads } \n "
417426 return res
418427
428+
419429@dataclass
420430class BadClassSignature (AppDaemonException ):
421431 class_name : str
@@ -439,7 +449,7 @@ class AppDependencyError(AppDaemonException):
439449 dep_name : str
440450 dependencies : set [str ]
441451
442- def __str__ (self , base : str = '' ):
452+ def __str__ (self , base : str = "" ):
443453 res = base
444454 res += f"\n all dependencies: { self .dependencies } "
445455 res += f"\n { self .rel_path } "
@@ -473,11 +483,8 @@ def __str__(self):
473483 res = f"Failed to import '{ self .module_name } '\n "
474484 if isinstance (self .__cause__ , ModuleNotFoundError ):
475485 res += "Import paths:\n "
476- paths = set (
477- p for p in sys .path
478- if Path (p ).is_relative_to (self .app_dir )
479- )
480- res += '\n ' .join (f' { p } ' for p in sorted (paths ))
486+ paths = set (p for p in sys .path if Path (p ).is_relative_to (self .app_dir ))
487+ res += "\n " .join (f" { p } " for p in sorted (paths ))
481488 return res
482489
483490
@@ -521,9 +528,9 @@ class InitializationFail(AppDaemonException):
521528 def __str__ (self ):
522529 res = f"initialize() method failed for app '{ self .app_name } '"
523530 if isinstance (self .__cause__ , TypeError ):
524- res += f' \n { self .__cause__ } '
525- res += ' \n initialize() should be structured like this:'
526- res += ' \n def initialize(self):'
531+ res += f" \n { self .__cause__ } "
532+ res += " \n initialize() should be structured like this:"
533+ res += " \n def initialize(self):"
527534 # res += '\n ...'
528535 return res
529536
@@ -544,7 +551,7 @@ class SequenceExecutionFail(AppDaemonException):
544551 def __str__ (self ):
545552 res = "Failed to execute sequence:"
546553 if isinstance (self .bad_seq , str ):
547- res += f' { self .bad_seq } '
554+ res += f" { self .bad_seq } "
548555 return res
549556
550557
0 commit comments