Skip to content
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

Support Arbitrary callbacks #2822

Merged
merged 22 commits into from
May 3, 2024
Merged

Support Arbitrary callbacks #2822

merged 22 commits into from
May 3, 2024

Conversation

T4rk1n
Copy link
Contributor

@T4rk1n T4rk1n commented Mar 29, 2024

- Allow no output callback
- Add global set_props
- Fix side update pattern ids
@ndrezn
Copy link
Member

ndrezn commented Apr 8, 2024

Going to add a few more tests, then we should be almost there!

@T4rk1n T4rk1n changed the title [WIP] Support Arbitrary callbacks Support Arbitrary callbacks Apr 11, 2024
@ndrezn ndrezn requested a review from emilykl April 15, 2024 15:04
dash/_utils.py Outdated Show resolved Hide resolved
dash/_callback.py Outdated Show resolved Hide resolved
dash/dash-renderer/src/actions/callbacks.ts Outdated Show resolved Hide resolved
@@ -331,9 +337,12 @@ def wrap_func(func):
def add_context(*args, **kwargs):
output_spec = kwargs.pop("outputs_list")
app_callback_manager = kwargs.pop("long_callback_manager", None)
callback_ctx = kwargs.pop("callback_context", {})
callback_ctx = kwargs.pop(
"callback_context", AttributeDict({"updated_props": {}})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does callback_ctx always need to contain an updated_props key?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember there is an error with some mocked tests.


callback_context = CallbackContext()


def set_props(component_id: typing.Union[str, dict], props: dict):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why the function signature for set_props isn't structured as 3 inputs: component_id, prop_name, value?

That to me seems more consistent with the rest of the Dash API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the same signature as the frontend set_props, we considered set_props("id", prop=value) but decided to stay with same API for frontend and backend.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's true, this pattern is a little more javascripty, but consistency is good and it could be convenient to be able to set multiple props of the same component in one call.

@@ -0,0 +1,7 @@
class ProxySetProps(dict):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of ProxySetProps? Maybe add a comment at the top of the file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a trick I found to make it work with background callbacks, when the value is set the handler is called and save the set_props data to be retrieved in the callback loop.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that mean background callbacks can update arbitrary props mid-execution and have that reflected in the app? If so that's super slick! Does it work to update the same prop(s) as the regular callback output(s), like progressive loading of the result?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the app is updated async like with the set_progress, it should be possible to update the same prop as the output.

@emilykl
Copy link
Contributor

emilykl commented Apr 22, 2024

Aside from my comments, LGTM. ✨

In general there are a number of places where you've added separate code paths for no_output=True vs. no_output=False and I wonder whether some of them can be condensed down to a single code path... for example if we need to run some processing on the list of outputs, plausibly the same code should work on an empty list of outputs without the need for an if/else (or could be made to work with some small tweaks).

.pylintrc Outdated Show resolved Hide resolved
if cb.get("no_output"):
outputs_list = []
elif not outputs_list:
# FIXME Old renderer support?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to support old renderer? Embedded? We should rebuild & test embedded with this update anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is what the comment on split_callback_id says for old embedded and new backend, it's pretty old now, maybe we can remove entirely.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into it when I'll update embedded next.

@T4rk1n T4rk1n requested a review from emilykl April 25, 2024 18:26
dash/_callback.py Outdated Show resolved Hide resolved
component_ids[id_str][prop] = vali
else:
if output_value is not None:
raise InvalidCallbackReturnValue(
Copy link
Contributor

@emilykl emilykl Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exposes a weird value for the "updating" part:
Screen Shot 2024-04-30 at 5 47 28 PM

Not crucial to fix as part of this PR but would be nice if it's an easy fix.

Copy link
Contributor Author

@T4rk1n T4rk1n May 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, the hash part is gonna be for all no-output callback error, let's try to remove it.

else:
if output_value is not None:
raise InvalidCallbackReturnValue(
f"No output callback received return value: {output_value}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should be "No-output" rather than "No output"

# A single dot within a dict id key or value is OK
# but in case of multiple dots together escape each dot
# with `\` so we don't mistake it for multi-outputs
hashed_inputs = None

def _hash_inputs():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor

@emilykl emilykl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM @T4rk1n , no more comments from me. 🎉

Could you double-check with @alexcjohnson whether his questions have been resolved before merging?

@T4rk1n T4rk1n merged commit a5a9e4e into dev May 3, 2024
3 checks passed
@T4rk1n T4rk1n deleted the feat/global-set-props branch May 3, 2024 13:20
@T4rk1n T4rk1n mentioned this pull request May 9, 2024
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants