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

Open
wants to merge 20 commits into
base: dev
Choose a base branch
from
Open

Support Arbitrary callbacks #2822

wants to merge 20 commits into from

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
Contributor

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
Contributor

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
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
Copy link
Contributor

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:

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)

Copy link
Contributor Author

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:

dash/dash/_utils.py

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('.')) {
Copy link
Contributor

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?
Copy link
Contributor

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.

@T4rk1n T4rk1n requested a review from emilykl April 25, 2024 18:26
dash/_callback.py Outdated Show resolved Hide resolved
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