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
base: dev
Are you sure you want to change the base?
Conversation
T4rk1n
commented
Mar 29, 2024
- Allow no output callback, resolve Allow for "null" / no output callbacks #1549
- Add global set_props, resolve [Feature Request] Global set_props in backend callbacks. #2803
- Fix side update pattern ids, fix [Long Callback] Support pattern matching for long callbacks side updates (progress/cancel/running) #2111
- Allow no output callback - Add global set_props - Fix side update pattern ids
Going to add a few more tests, then we should be almost there! |
@@ -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": {}}) |
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 does callback_ctx
always need to contain an updated_props
key?
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 remember there is an error with some mocked tests.
|
||
callback_context = CallbackContext() | ||
|
||
|
||
def set_props(component_id: typing.Union[str, dict], props: dict): |
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 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.
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.
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.
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.
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): |
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's the purpose of ProxySetProps
? Maybe add a comment at the top of the file.
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.
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.
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.
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?
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, the app is updated async like with the set_progress, it should be possible to update the same prop as the output.
Aside from my comments, LGTM. ✨ In general there are a number of places where you've added separate code paths for |
dash/_callback_context.py
Outdated
def set_props(self, component_id: typing.Union[str, dict], props: dict): | ||
ctx_value = _get_context_value() | ||
if isinstance(component_id, dict): | ||
ctx_value.updated_props[json.dumps(component_id)] = 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.
Does plain json.dumps
work here? It's not quite the same as we have for stringifying component IDs in the callback definition:
Lines 46 to 59 in 9a4a479
def component_id_str(self): | |
i = self.component_id | |
def _dump(v): | |
return json.dumps(v, sort_keys=True, separators=(",", ":")) | |
def _json(k, v): | |
vstr = v.to_json() if hasattr(v, "to_json") else json.dumps(v) | |
return f"{json.dumps(k)}:{vstr}" | |
if isinstance(i, dict): | |
return "{" + ",".join(_json(k, i[k]) for k in sorted(i)) + "}" | |
return i |
For set_props
we don't need to handle wildcards, but I'd have thought we still want to sort keys and compress spaces.
(though looks like _dump
there is unused and should be removed)
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.
There is already a stringify_id
here:
Lines 169 to 172 in 9a4a479
def stringify_id(id_): | |
if isinstance(id_, dict): | |
return json.dumps(id_, sort_keys=True, separators=(",", ":")) | |
return id_ |
I think the output is the same and is used in the base component, we should use the same everywhere.
function sideUpdate(outputs: any, dispatch: any) { | ||
toPairs(outputs).forEach(([id, value]) => { | ||
let componentId, propName; | ||
if (id.includes('.')) { |
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.
Can't a dict ID contain .
?
if cb.get("no_output"): | ||
outputs_list = [] | ||
elif not outputs_list: | ||
# FIXME Old renderer support? |
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 do we want to support old renderer? Embedded? We should rebuild & test embedded with this update anyway.
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.
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.