1
1
import collections
2
2
import hashlib
3
3
from functools import wraps
4
+ from typing import Callable , Optional , Any
4
5
5
6
import flask
6
7
@@ -67,6 +68,7 @@ def callback(
67
68
cancel = None ,
68
69
manager = None ,
69
70
cache_args_to_ignore = None ,
71
+ on_error : Optional [Callable [[Exception ], Any ]] = None ,
70
72
** _kwargs ,
71
73
):
72
74
"""
@@ -137,6 +139,10 @@ def callback(
137
139
this should be a list of argument indices as integers.
138
140
:param interval:
139
141
Time to wait between the long callback update requests.
142
+ :param on_error:
143
+ Function to call when the callback raises an exception. Receives the
144
+ exception object as first argument. The callback_context can be used
145
+ to access the original callback inputs, states and output.
140
146
"""
141
147
142
148
long_spec = None
@@ -186,6 +192,7 @@ def callback(
186
192
long = long_spec ,
187
193
manager = manager ,
188
194
running = running ,
195
+ on_error = on_error ,
189
196
)
190
197
191
198
@@ -226,7 +233,7 @@ def insert_callback(
226
233
long = None ,
227
234
manager = None ,
228
235
running = None ,
229
- dynamic_creator = False ,
236
+ dynamic_creator : Optional [ bool ] = False ,
230
237
no_output = False ,
231
238
):
232
239
if prevent_initial_call is None :
@@ -272,8 +279,16 @@ def insert_callback(
272
279
return callback_id
273
280
274
281
275
- # pylint: disable=R0912, R0915
276
- def register_callback ( # pylint: disable=R0914
282
+ def _set_side_update (ctx , response ) -> bool :
283
+ side_update = dict (ctx .updated_props )
284
+ if len (side_update ) > 0 :
285
+ response ["sideUpdate" ] = side_update
286
+ return True
287
+ return False
288
+
289
+
290
+ # pylint: disable=too-many-branches,too-many-statements
291
+ def register_callback (
277
292
callback_list , callback_map , config_prevent_initial_callbacks , * _args , ** _kwargs
278
293
):
279
294
(
@@ -297,6 +312,7 @@ def register_callback( # pylint: disable=R0914
297
312
long = _kwargs .get ("long" )
298
313
manager = _kwargs .get ("manager" )
299
314
running = _kwargs .get ("running" )
315
+ on_error = _kwargs .get ("on_error" )
300
316
if running is not None :
301
317
if not isinstance (running [0 ], (list , tuple )):
302
318
running = [running ]
@@ -342,6 +358,8 @@ def add_context(*args, **kwargs):
342
358
"callback_context" , AttributeDict ({"updated_props" : {}})
343
359
)
344
360
callback_manager = long and long .get ("manager" , app_callback_manager )
361
+ error_handler = on_error or kwargs .pop ("app_on_error" , None )
362
+
345
363
if has_output :
346
364
_validate .validate_output_spec (insert_output , output_spec , Output )
347
365
@@ -351,7 +369,7 @@ def add_context(*args, **kwargs):
351
369
args , inputs_state_indices
352
370
)
353
371
354
- response = {"multi" : True }
372
+ response : dict = {"multi" : True }
355
373
has_update = False
356
374
357
375
if long is not None :
@@ -440,10 +458,24 @@ def add_context(*args, **kwargs):
440
458
isinstance (output_value , dict )
441
459
and "long_callback_error" in output_value
442
460
):
443
- error = output_value .get ("long_callback_error" )
444
- raise LongCallbackError (
461
+ error = output_value .get ("long_callback_error" , {} )
462
+ exc = LongCallbackError (
445
463
f"An error occurred inside a long callback: { error ['msg' ]} \n { error ['tb' ]} "
446
464
)
465
+ if error_handler :
466
+ output_value = error_handler (exc )
467
+
468
+ if output_value is None :
469
+ output_value = NoUpdate ()
470
+ # set_props from the error handler uses the original ctx
471
+ # instead of manager.get_updated_props since it runs in the
472
+ # request process.
473
+ has_update = (
474
+ _set_side_update (callback_ctx , response )
475
+ or output_value is not None
476
+ )
477
+ else :
478
+ raise exc
447
479
448
480
if job_running and output_value is not callback_manager .UNDEFINED :
449
481
# cached results.
@@ -462,10 +494,22 @@ def add_context(*args, **kwargs):
462
494
if output_value is callback_manager .UNDEFINED :
463
495
return to_json (response )
464
496
else :
465
- output_value = _invoke_callback (func , * func_args , ** func_kwargs )
466
-
467
- if NoUpdate .is_no_update (output_value ):
468
- raise PreventUpdate
497
+ try :
498
+ output_value = _invoke_callback (func , * func_args , ** func_kwargs )
499
+ except PreventUpdate as err :
500
+ raise err
501
+ except Exception as err : # pylint: disable=broad-exception-caught
502
+ if error_handler :
503
+ output_value = error_handler (err )
504
+
505
+ # If the error returns nothing, automatically puts NoUpdate for response.
506
+ if output_value is None :
507
+ if not multi :
508
+ output_value = NoUpdate ()
509
+ else :
510
+ output_value = [NoUpdate for _ in output_spec ]
511
+ else :
512
+ raise err
469
513
470
514
component_ids = collections .defaultdict (dict )
471
515
@@ -487,12 +531,12 @@ def add_context(*args, **kwargs):
487
531
)
488
532
489
533
for val , spec in zip (flat_output_values , output_spec ):
490
- if isinstance (val , NoUpdate ):
534
+ if NoUpdate . is_no_update (val ):
491
535
continue
492
536
for vali , speci in (
493
537
zip (val , spec ) if isinstance (spec , list ) else [[val , spec ]]
494
538
):
495
- if not isinstance (vali , NoUpdate ):
539
+ if not NoUpdate . is_no_update (vali ):
496
540
has_update = True
497
541
id_str = stringify_id (speci ["id" ])
498
542
prop = clean_property_name (speci ["property" ])
@@ -506,10 +550,7 @@ def add_context(*args, **kwargs):
506
550
flat_output_values = []
507
551
508
552
if not long :
509
- side_update = dict (callback_ctx .updated_props )
510
- if len (side_update ) > 0 :
511
- has_update = True
512
- response ["sideUpdate" ] = side_update
553
+ has_update = _set_side_update (callback_ctx , response ) or has_update
513
554
514
555
if not has_update :
515
556
raise PreventUpdate
0 commit comments