-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add callback on_error handler #2903
Conversation
@@ -67,6 +68,7 @@ def callback( | |||
cancel=None, | |||
manager=None, | |||
cache_args_to_ignore=None, | |||
on_error: Optional[Callable[[Exception], Any]] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type hints ❤️ ❤️
@@ -414,7 +414,7 @@ max-bool-expr=5 | |||
max-branches=15 | |||
|
|||
# Maximum number of locals for function / method body | |||
max-locals=20 | |||
max-locals=25 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to move some of the logic to a sub-function rather than increasing this limit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I forgot to mention in the PR description, this rule is broken cannot be concatenated with other rule disabling so i had to change this instead.
@@ -272,8 +279,8 @@ def insert_callback( | |||
return callback_id | |||
|
|||
|
|||
# pylint: disable=R0912, R0915 | |||
def register_callback( # pylint: disable=R0914 | |||
# pylint: disable=too-many-branches,too-many-statements |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question as above -- can this function be lightened a bit by moving some logic to sub-functions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@T4rk1n any thoughts on ^ ? Not to be a stickler and it's not a blocker for merging this PR but would love to see this function simplified if possible rather than disabling the linter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't disable anything in this PR, just changed to the rulename instead of number. The settings for these rules is kinda low imo,
- too-many-branches=15 is kinda high, but this function is wrapped multiple times in decorators which is why it's triggered here.
- too-many-statements=50 is kinda low for a project this size and keep getting triggered everywhere, we also have functions within functions which actually separate the code inside but the linter isn't too smart here and the top function accumulates all the statements.
- variable numbers also get from the fact this is multiple functions wrapped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see now, thanks for the explanation. 👍
@@ -462,7 +473,15 @@ def add_context(*args, **kwargs): | |||
if output_value is callback_manager.UNDEFINED: | |||
return to_json(response) | |||
else: | |||
output_value = _invoke_callback(func, *func_args, **func_kwargs) | |||
try: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯
dash/_callback.py
Outdated
try: | ||
output_value = _invoke_callback(func, *func_args, **func_kwargs) | ||
except Exception as err: # pylint: disable=broad-exception-caught | ||
if on_error: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not do something like on_error_func = on_error or app_on_error
further up in the code, to avoid this extra elif
@T4rk1n This rocks!!! I'm going to play around with this feature tomorrow and try to break it. Couple of questions in the meantime:
|
The global error handler needs the same outputs, to know the outputs the callback_context can be used. I just noticed that
|
If the If this is also the case, then this is not really a global error handler as it can fail unexpectedly and is no different than placing error handling inside of the callback in the first place? I think the |
The handler return values goes thru the same validation as the original callback function, the exception is raised.
Can still do |
I like the idea of automatically adding a |
Another thing, is if someone does |
Just tried this out and it looks like currently it does (cc @T4rk1n). I suppose the fix would be to modify the try/except so that it catches every type of error except |
Yes, I think that would be less confusing, the outputs would still be able to be changed via set_props. |
@emilykl can you please give this one final look and approve the merge? |
dash/_callback.py
Outdated
if error_handler: | ||
error_handler(err) | ||
if not multi: | ||
output_value = NoUpdate() | ||
else: | ||
output_value = [NoUpdate for _ in output_spec] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if error_handler: | |
error_handler(err) | |
if not multi: | |
output_value = NoUpdate() | |
else: | |
output_value = [NoUpdate for _ in output_spec] | |
if error_handler: | |
output_value = error_handler(err) | |
if not multi: | |
output_value = output_value or NoUpdate() | |
else: | |
output_value = output_value or [NoUpdate() for _ in output_spec] |
@T4rk1n I feel that we should still allow the dev to return outputs from the error handler -- we should supply the NoUpdate()
outputs only if the error handler doesn't return anything. (I realize the dev could still update the outputs via set_props()
, but I don't see any downside to also allowing them to output the "normal" way.) Happy to discuss further if you disagree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this wouldn't the dev just handle the error inside of the callback itself, as no two outputs are going to be the same?
Also, I thought that this would return a regular output? The no_update seems to be only supplied if there was no returned value? -- wait, I read your suggestion @emilykl and thought it was the code in question and not your suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing it like this could lead to situation where you wanted to output something that evaluate to false but it would get a no_update
instead. see #2906 (comment), a components which has no children props will always be false.
Best we could do is if output_value is not None
, but that brings back the difference between undefined vs none, which python lacks an undefined type and None could be a valid prop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about this:
if error_handler:
output_value = error_handler(err)
if output_value is not None:
# Code to execute if a value is returned
'''output no change'''
else:
# Code to execute if no value is returned
output_value = [NoUpdate() for _ in output_spec] if multi else NoUpdate()
Then have it documented that this will pass NoUpdate
in error if you are really trying to pass a None
as a single output. If this is the case, then the dev should instead place: [None]
For things that cant be solved by this, the error should be handled inside of the callback itself before it bubbles up to the error handler. One example I just tested was trying to reset a dropdown value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this wouldn't the dev just handle the error inside of the callback itself, as no two outputs are going to be the same?
For the global error handler, yes, this is true -- I can't imagine a situation where you'd want to return specific outputs from the global error handler. But for a callback-specific error handler the dev might want to return specific outputs.
python lacks an undefined type and
None
could be a valid prop.
Ugh, this is a good point. Thanks Python.
I think it would be OK to check if output_value is not None
, and then document that in the specific edge case where you do actually want to return None
, you have to use set_props()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
callback-specific error handler
unless they are reusing, they could still handle inside of the callback, but our syntax is still easier I guess, haha. Rather than them having to wrap the whole function in a try except clause.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to NoOutput only if output_value
is None, the output could be set to [None]
if the dev want to set the output prop to null. cc @LiamConnors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the output could be set to [None] if the dev want to set the output prop to null.
FWIW, does this actually work? Typically I have gotten errors when wrapping a single output in a list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it works, but only would work if the output definition in the callback is [Output(id, prop)]
try: | ||
output_value = _invoke_callback(func, *func_args, **func_kwargs) | ||
except PreventUpdate as err: | ||
raise err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
dash/_callback.py
Outdated
error_handler(exc) | ||
output_value = NoUpdate() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see https://github.com/plotly/dash/pull/2903/files#r1669336763
error_handler(exc) | |
output_value = NoUpdate() | |
output_value = error_handler(exc) | |
output_value = output_value or NoUpdate() |
@T4rk1n See my comments regarding outputs from the error handler. Otherwise LGTM. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@T4rk1n Could you maybe update the test cases so there's one test case with an error handler function which returns callback outputs?
In any case, looks good! 🚀
Add two error handler to call when a callback raise an exception, the return value of the handler is instead used as output. The callback context can be used to differentiate the erroring callbacks and access the original inputs and states.
A global error handler can be added with a function reference to the Dash constructor: eg:
app = Dash(on_error=handler)
.And a local handler for particular callback can be added to individual callback: eg:
@callback(..., on_error=handler)
.The signature of the handler function is:
def func(error: Exception) -> None
.set_props
can be used to set extra props or the original output.Example:
Partially based of #2829 to catch exception and call a function instead of a decorator pattern and also works with background callbacks.
Closes #2348