From 6771920cb70ab38de269ed93553a976f600b55c2 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Sun, 27 Feb 2022 17:31:58 -0700 Subject: [PATCH 01/13] Add triggered_id to callback_context --- dash/__init__.py | 2 +- dash/_callback_context.py | 15 ++++++++++- .../callbacks/test_callback_context.py | 27 ++++++++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/dash/__init__.py b/dash/__init__.py index 7e59891b8b..1b64bc170a 100644 --- a/dash/__init__.py +++ b/dash/__init__.py @@ -19,7 +19,7 @@ from . import html # noqa: F401,E402 from . import dash_table # noqa: F401,E402 from .version import __version__ # noqa: F401,E402 -from ._callback_context import callback_context # noqa: F401,E402 +from ._callback_context import callback_context, ctx # noqa: F401,E402 from ._callback import callback, clientside_callback # noqa: F401,E402 from ._get_paths import ( # noqa: F401,E402 get_asset_url, diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 585de7fc47..fdb21c38a4 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -1,6 +1,6 @@ import functools import warnings - +import json import flask from . import exceptions @@ -54,6 +54,18 @@ def triggered(self): # look empty, but you can still do `triggered[0]["prop_id"].split(".")` return getattr(flask.g, "triggered_inputs", []) or falsy_triggered + @property + @has_context + def triggered_ids(self): + triggered = getattr(flask.g, "triggered_inputs", []) + ids = {} + for item in triggered: + component_id, _, _ = item["prop_id"].rpartition(".") + ids[item["prop_id"]] = component_id + if component_id.startswith("{"): + ids[item["prop_id"]] = json.loads(component_id) + return ids + @property @has_context def args_grouping(self): @@ -145,3 +157,4 @@ def using_outputs_grouping(self): callback_context = CallbackContext() +ctx = CallbackContext() diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py index e19c5e63ac..6f382a7a46 100644 --- a/tests/integration/callbacks/test_callback_context.py +++ b/tests/integration/callbacks/test_callback_context.py @@ -2,7 +2,7 @@ import operator import pytest -from dash import Dash, Input, Output, html, dcc, callback_context +from dash import Dash, Input, Output, html, dcc, callback_context, ctx from dash.exceptions import PreventUpdate, MissingCallbackContextException import dash.testing.wait as wait @@ -330,3 +330,28 @@ def update_results(n1, n2, nsum): assert len(keys1) == 2 assert "sum-number.value" in keys1 assert "input-number-2.value" in keys1 + + +def test_cbcx007_triggered_id(dash_duo): + app = Dash(__name__) + + btns = ["btn-{}".format(x) for x in range(1, 6)] + + app.layout = html.Div( + [html.Div([html.Button(btn, id=btn) for btn in btns]), html.Div(id="output")] + ) + + @app.callback(Output("output", "children"), [Input(x, "n_clicks") for x in btns]) + def on_click(*args): + if not ctx.triggered: + raise PreventUpdate + for btn in btns: + if btn in ctx.triggered_ids.values(): + return f"Just clicked {btn}" + + dash_duo.start_server(app) + + for i in range(1, 5): + for btn in btns: + dash_duo.find_element("#" + btn).click() + dash_duo.wait_for_text_to_equal("#output", f"Just clicked {btn}") From 2728d83e2235e24aff497e271af3f992c843f62e Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Sun, 27 Feb 2022 18:12:20 -0700 Subject: [PATCH 02/13] added test --- .../callbacks/test_callback_context.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py index 6f382a7a46..4ffaf6a278 100644 --- a/tests/integration/callbacks/test_callback_context.py +++ b/tests/integration/callbacks/test_callback_context.py @@ -2,7 +2,7 @@ import operator import pytest -from dash import Dash, Input, Output, html, dcc, callback_context, ctx +from dash import Dash, ALL, Input, Output, html, dcc, callback_context, ctx from dash.exceptions import PreventUpdate, MissingCallbackContextException import dash.testing.wait as wait @@ -355,3 +355,29 @@ def on_click(*args): for btn in btns: dash_duo.find_element("#" + btn).click() dash_duo.wait_for_text_to_equal("#output", f"Just clicked {btn}") + + +def test_cbcx008_triggered_id_pmc(dash_duo): + + app = Dash() + app.layout = html.Div( + [ + html.Button("Click me", id={"type": "btn", "index": "myindex"}), + html.Div(id="output"), + ] + ) + + @app.callback( + Output("output", "children"), Input({"type": "btn", "index": ALL}, "n_clicks") + ) + def func(n_clicks): + if ctx.triggered: + triggered_id, dict_id = next(iter(ctx.triggered_ids.items())) + if dict_id == {"type": "btn", "index": "myindex"}: + return dict_id["index"] + + dash_duo.start_server(app) + dash_duo.find_element( + '#\\{\\"index\\"\\:\\"myindex\\"\\,\\"type\\"\\:\\"btn\\"\\}' + ).click() + dash_duo.wait_for_text_to_equal("#output", "myindex") From d19f04c9529d624a8d8f9d02f047c4e972f9d4db Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Thu, 3 Mar 2022 14:20:45 -0700 Subject: [PATCH 03/13] extended ctx.arg_grouping and changed it to AttributeDict --- dash/_callback_context.py | 54 ++++++++++++++++++++++++++++++++++++++- dash/_grouping.py | 5 ++-- dash/_utils.py | 2 +- dash/_validate.py | 5 ++++ dash/dash.py | 3 +++ 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/dash/_callback_context.py b/dash/_callback_context.py index fdb21c38a4..879bf209e0 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -1,6 +1,7 @@ import functools import warnings import json +from copy import deepcopy import flask from . import exceptions @@ -69,7 +70,58 @@ def triggered_ids(self): @property @has_context def args_grouping(self): - return getattr(flask.g, "args_grouping", []) + triggered = getattr(flask.g, "triggered_inputs", []) + triggered = [item["prop_id"] for item in triggered] + grouping = getattr(flask.g, "args_grouping", {}) + + def update_args_grouping(g): + if isinstance(g, dict) and "id" in g: + prop_id = ".".join((g["id"], g["property"])) + + new_values = { + "value": g.get("value"), + "id": g["id"] + if not g["id"].startswith("{") + else json.loads(g["id"]), + "property": g["property"], + "triggered": prop_id in triggered, + } + g.update(new_values) + + def recursive_update(g): + if isinstance(g, (tuple, list)): + for i in g: + update_args_grouping(i) + recursive_update(i) + if isinstance(g, dict): + for i in g.values(): + update_args_grouping(i) + recursive_update(i) + + recursive_update(grouping) + + return grouping + + # todo not sure whether we need this, but it removes a level of nesting so + # you don't need to use `.value` to get the value. + @property + @has_context + def args_grouping_values(self): + grouping = getattr(flask.g, "args_grouping", {}) + grouping = deepcopy(grouping) + + def recursive_update(g): + if isinstance(g, (tuple, list)): + for i in g: + recursive_update(i) + if isinstance(g, dict): + for k, v in g.items(): + if isinstance(v, dict) and "id" in v: + g[k] = v["value"] + recursive_update(v) + + recursive_update(grouping) + return grouping @property @has_context diff --git a/dash/_grouping.py b/dash/_grouping.py index 89d62de8aa..790c666fb5 100644 --- a/dash/_grouping.py +++ b/dash/_grouping.py @@ -14,6 +14,7 @@ """ from dash.exceptions import InvalidCallbackReturnValue +from ._utils import AttributeDict def flatten_grouping(grouping, schema=None): @@ -127,14 +128,14 @@ def map_grouping(fn, grouping): return [map_grouping(fn, g) for g in grouping] if isinstance(grouping, dict): - return {k: map_grouping(fn, g) for k, g in grouping.items()} + return AttributeDict({k: map_grouping(fn, g) for k, g in grouping.items()}) return fn(grouping) def make_grouping_by_key(schema, source, default=None): """ - Create a grouping from a schema by ujsing the schema's scalar values to look up + Create a grouping from a schema by using the schema's scalar values to look up items in the provided source object. :param schema: A grouping of potential keys in source diff --git a/dash/_utils.py b/dash/_utils.py index 31de7e88cb..e5397e2088 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -159,7 +159,7 @@ def stringify_id(id_): def inputs_to_dict(inputs_list): - inputs = {} + inputs = AttributeDict() for i in inputs_list: inputsi = i if isinstance(i, list) else [i] for ii in inputsi: diff --git a/dash/_validate.py b/dash/_validate.py index 0318c16d22..b8613126c3 100644 --- a/dash/_validate.py +++ b/dash/_validate.py @@ -136,6 +136,11 @@ def validate_and_group_input_args(flat_args, arg_index_grouping): if isinstance(arg_index_grouping, dict): func_args = [] func_kwargs = args_grouping + for key in func_kwargs: + if not key.isidentifier(): + raise exceptions.CallbackException( + f"{key} is not a valid Python variable name" + ) elif isinstance(arg_index_grouping, (tuple, list)): func_args = list(args_grouping) func_kwargs = {} diff --git a/dash/dash.py b/dash/dash.py index c0b6cb410d..210ce6931a 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1275,6 +1275,7 @@ def callback(_triggers, user_store_data, user_callback_args): def dispatch(self): body = flask.request.get_json() + flask.g.inputs_list = inputs = body.get( # pylint: disable=assigning-non-slot "inputs", [] ) @@ -1309,9 +1310,11 @@ def dispatch(self): # Add args_grouping inputs_state_indices = cb["inputs_state_indices"] inputs_state = inputs + state + inputs_state = [AttributeDict(i) for i in inputs_state] args_grouping = map_grouping( lambda ind: inputs_state[ind], inputs_state_indices ) + flask.g.args_grouping = args_grouping # pylint: disable=assigning-non-slot flask.g.using_args_grouping = ( # pylint: disable=assigning-non-slot not isinstance(inputs_state_indices, int) From 2801e61c6955597c9dd2b871db3909a392b2436b Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Fri, 4 Mar 2022 10:04:42 -0700 Subject: [PATCH 04/13] clean-up and fix for pmc --- dash/_callback_context.py | 9 ++++----- dash/_utils.py | 10 ++++++++++ dash/dash.py | 4 +++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 879bf209e0..a43d3a828f 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -5,6 +5,7 @@ import flask from . import exceptions +from ._utils import stringify_id def has_context(func): @@ -76,14 +77,12 @@ def args_grouping(self): def update_args_grouping(g): if isinstance(g, dict) and "id" in g: - prop_id = ".".join((g["id"], g["property"])) + str_id = stringify_id(g["id"]) + prop_id = "{}.{}".format(str_id, g["property"]) new_values = { "value": g.get("value"), - "id": g["id"] - if not g["id"].startswith("{") - else json.loads(g["id"]), - "property": g["property"], + "str_id": str_id, "triggered": prop_id in triggered, } g.update(new_values) diff --git a/dash/_utils.py b/dash/_utils.py index e5397e2088..ccc5072b0d 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -168,6 +168,16 @@ def inputs_to_dict(inputs_list): return inputs +def convert_to_AttributeDict(nested_list): + new_dict = [] + for i in nested_list: + if isinstance(i, dict): + new_dict.append(AttributeDict(i)) + else: + new_dict.append([AttributeDict(ii) for ii in i]) + return new_dict + + def inputs_to_vals(inputs): return [ [ii.get("value") for ii in i] if isinstance(i, list) else i.get("value") diff --git a/dash/dash.py b/dash/dash.py index 210ce6931a..834a3e0568 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -48,6 +48,7 @@ patch_collections_abc, split_callback_id, to_json, + convert_to_AttributeDict, ) from . import _callback from . import _get_paths @@ -1310,7 +1311,8 @@ def dispatch(self): # Add args_grouping inputs_state_indices = cb["inputs_state_indices"] inputs_state = inputs + state - inputs_state = [AttributeDict(i) for i in inputs_state] + inputs_state = convert_to_AttributeDict(inputs_state) + args_grouping = map_grouping( lambda ind: inputs_state[ind], inputs_state_indices ) From 1d957eab1880c2dbfe2d43107e7666e943d8217a Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Fri, 4 Mar 2022 12:57:22 -0700 Subject: [PATCH 05/13] fixed test --- tests/integration/callbacks/test_wildcards.py | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index 7359c55671..4f5a403e12 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -1,6 +1,7 @@ import pytest import re from selenium.webdriver.common.keys import Keys +import json from dash.testing import wait import dash @@ -10,6 +11,12 @@ from tests.assets.grouping_app import grouping_app +def stringify_id(id_): + if isinstance(id_, dict): + return json.dumps(id_, sort_keys=True, separators=(",", ":")) + return id_ + + def css_escape(s): sel = re.sub("[\\{\\}\\\"\\'.:,]", lambda m: "\\" + m.group(0), s) print(sel) @@ -413,14 +420,38 @@ def assert_callback_context(items_text): args_grouping = dict( items=dict( all=[ - {"id": {"item": i}, "property": "children", "value": text} + { + "id": {"item": i}, + "property": "children", + "value": text, + "str_id": stringify_id({"item": i}), + "triggered": False, + } for i, text in enumerate(items_text[:-1]) ], - new=dict(id="new-item", property="value", value=items_text[-1]), + new=dict( + id="new-item", + property="value", + value=items_text[-1], + str_id="new-item", + triggered=False, + ), ), triggers=[ - {"id": "add", "property": "n_clicks", "value": len(items_text)}, - {"id": "new-item", "property": "n_submit"}, + { + "id": "add", + "property": "n_clicks", + "value": len(items_text), + "str_id": "add", + "triggered": True, + }, + { + "id": "new-item", + "property": "n_submit", + "value": None, + "str_id": "new-item", + "triggered": False, + }, ], ) dash_duo.wait_for_text_to_equal("#cc-args-grouping", repr(args_grouping)) From 6fc9900b342707ef239e4f2ef5a744375cc1bf51 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Sun, 6 Mar 2022 19:10:20 -0700 Subject: [PATCH 06/13] added AttributeDict to PMC ids added to .start method in AttributeDict --- dash/_callback_context.py | 9 ++++++--- dash/_utils.py | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/dash/_callback_context.py b/dash/_callback_context.py index a43d3a828f..3ed4b903f7 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -5,7 +5,7 @@ import flask from . import exceptions -from ._utils import stringify_id +from ._utils import stringify_id, AttributeDict def has_context(func): @@ -60,12 +60,12 @@ def triggered(self): @has_context def triggered_ids(self): triggered = getattr(flask.g, "triggered_inputs", []) - ids = {} + ids = AttributeDict({}) for item in triggered: component_id, _, _ = item["prop_id"].rpartition(".") ids[item["prop_id"]] = component_id if component_id.startswith("{"): - ids[item["prop_id"]] = json.loads(component_id) + ids[item["prop_id"]] = AttributeDict(json.loads(component_id)) return ids @property @@ -84,6 +84,9 @@ def update_args_grouping(g): "value": g.get("value"), "str_id": str_id, "triggered": prop_id in triggered, + "id": AttributeDict(g["id"]) + if isinstance(g["id"], dict) + else g["id"], } g.update(new_values) diff --git a/dash/_utils.py b/dash/_utils.py index ccc5072b0d..1c658b09e3 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -119,6 +119,8 @@ def first(self, *names): value = self.get(name) if value: return value + if names == (): + return next(iter(self), {}) def create_callback_id(output): From 49a5e59ee464917f4a5a196548e1e9185dfcbd64 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Thu, 10 Mar 2022 10:33:31 -0700 Subject: [PATCH 07/13] added triggered_id and renamed triggered_prop_ids --- dash/_callback_context.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 3ed4b903f7..48afd39c19 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -58,7 +58,7 @@ def triggered(self): @property @has_context - def triggered_ids(self): + def triggered_prop_ids(self): triggered = getattr(flask.g, "triggered_inputs", []) ids = AttributeDict({}) for item in triggered: @@ -68,6 +68,15 @@ def triggered_ids(self): ids[item["prop_id"]] = AttributeDict(json.loads(component_id)) return ids + @property + @has_context + def triggered_id(self): + component_id = None + if self.triggered: + prop_id = self.triggered_prop_ids.first() + component_id = self.triggered_prop_ids[prop_id] + return component_id + @property @has_context def args_grouping(self): From 67f56d09d70e77701d2ae9a002aa330202da118b Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Sun, 13 Mar 2022 14:50:44 -0700 Subject: [PATCH 08/13] added docstrings --- dash/_callback_context.py | 67 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 48afd39c19..c5ce432fcc 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -50,6 +50,18 @@ def states(self): @property @has_context def triggered(self): + """ + Returns a list of all the Input props that changed and caused the callback to execute. It is empty when the callback is + called on initial load, unless an Input prop got its value from another initial callback. Callbacks triggered + by user actions typically have one item in triggered, unless the same action changes two props at once or + the callback has several Input props that are all modified by another callback based on a single user action. + + Example: To get the id of the component that triggered the callback: + `component_id = ctx.triggered[0]['prop_id'].split('.')[0]` + + Example: To detect initial call, empty triggered is not really empty, it's falsy so that you can use: + `if ctx.triggered:` + """ # For backward compatibility: previously `triggered` always had a # value - to avoid breaking existing apps, add a dummy item but # make the list still look falsy. So `if ctx.triggered` will make it @@ -59,6 +71,26 @@ def triggered(self): @property @has_context def triggered_prop_ids(self): + """ + Returns a dictionary of all the Input props that changed and caused the callback to execute. It is empty when the callback is + called on initial load, unless an Input prop got its value from another initial callback. Callbacks triggered + by user actions typically have one item in triggered, unless the same action changes two props at once or + the callback has several Input props that are all modified by another callback based on a single user action. + + triggered_prop_ids (dict): + - keys (str) : the triggered "prop_id" composed of "component_id.component_property" + - values (str or dict): the id of the component that triggered the callback. Will be the dict id for pattern matching callbacks + + Example - regular callback + {"btn-1.n_clicks": "btn-1"} + + Example - pattern matching callbacks: + {'{"index":0,"type":"filter-dropdown"}.value': {"index":0,"type":"filter-dropdown"}} + + Example usage: + `if "btn-1.n_clicks" in ctx.triggered_prop_ids: + do_something()` + """ triggered = getattr(flask.g, "triggered_inputs", []) ids = AttributeDict({}) for item in triggered: @@ -71,6 +103,17 @@ def triggered_prop_ids(self): @property @has_context def triggered_id(self): + """ + Returns the component id (str or dict) of the Input component that triggered the callback. + + Note - use `triggered_prop_ids` if you need both the component id and the prop that triggered the callback or if + multiple Inputs triggered the callback. + + Example usage: + `if "btn-1" == ctx.triggered_id: + do_something()` + + """ component_id = None if self.triggered: prop_id = self.triggered_prop_ids.first() @@ -80,6 +123,30 @@ def triggered_id(self): @property @has_context def args_grouping(self): + """ + args_grouping is a dict of the inputs used with flexible callback signatures. The keys are the variable names + and the values are dictionaries containing: + - “id”: (string or dict) the component id. If it’s a pattern matching id, it will be a dict. + - “id_str”: (str) for pattern matching ids, it’s the strigified dict id with no white spaces. + - “property”: (str) The component property used in the callback. + - “value”: the value of the component property at the time the callback was fired. + - “triggered”: (bool)Whether this input triggered the callback. + + Example usage: + @app.callback( + Output("container", "children"), + inputs=dict(btn1=Input("btn-1", "n_clicks"), btn2=Input("btn-2", "n_clicks")), + ) + def display(btn1, btn2): + c = ctx.args_grouping + if c.btn1.triggered: + return f"Button 1 clicked {btn1} times" + elif c.btn2.triggered: + return f"Button 2 clicked {btn2} times" + else: + return "No clicks yet" + + """ triggered = getattr(flask.g, "triggered_inputs", []) triggered = [item["prop_id"] for item in triggered] grouping = getattr(flask.g, "args_grouping", {}) From df6874a214fee97b108627b084d098d640086cba Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Sun, 13 Mar 2022 16:09:10 -0700 Subject: [PATCH 09/13] fixed tests --- tests/integration/callbacks/test_callback_context.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py index 4ffaf6a278..1080687a2e 100644 --- a/tests/integration/callbacks/test_callback_context.py +++ b/tests/integration/callbacks/test_callback_context.py @@ -346,7 +346,8 @@ def on_click(*args): if not ctx.triggered: raise PreventUpdate for btn in btns: - if btn in ctx.triggered_ids.values(): + if btn in ctx.triggered_prop_ids.values(): + assert btn == ctx.triggered_id return f"Just clicked {btn}" dash_duo.start_server(app) @@ -372,11 +373,15 @@ def test_cbcx008_triggered_id_pmc(dash_duo): ) def func(n_clicks): if ctx.triggered: - triggered_id, dict_id = next(iter(ctx.triggered_ids.items())) + triggered_id, dict_id = next(iter(ctx.triggered_prop_ids.items())) + + assert dict_id == ctx.triggered_id + if dict_id == {"type": "btn", "index": "myindex"}: return dict_id["index"] dash_duo.start_server(app) + dash_duo.find_element( '#\\{\\"index\\"\\:\\"myindex\\"\\,\\"type\\"\\:\\"btn\\"\\}' ).click() From 2d33fc270fc9ee4c5bd60a5e2ca700d974301a8a Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Thu, 21 Apr 2022 14:04:18 -0700 Subject: [PATCH 10/13] update after review --- dash/__init__.py | 3 ++- dash/_callback_context.py | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/dash/__init__.py b/dash/__init__.py index 1b64bc170a..c90d6463c7 100644 --- a/dash/__init__.py +++ b/dash/__init__.py @@ -19,10 +19,11 @@ from . import html # noqa: F401,E402 from . import dash_table # noqa: F401,E402 from .version import __version__ # noqa: F401,E402 -from ._callback_context import callback_context, ctx # noqa: F401,E402 +from ._callback_context import callback_context # noqa: F401,E402 from ._callback import callback, clientside_callback # noqa: F401,E402 from ._get_paths import ( # noqa: F401,E402 get_asset_url, get_relative_path, strip_relative_path, ) +ctx = callback_context diff --git a/dash/_callback_context.py b/dash/_callback_context.py index c5ce432fcc..25e09d0da8 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -51,10 +51,11 @@ def states(self): @has_context def triggered(self): """ - Returns a list of all the Input props that changed and caused the callback to execute. It is empty when the callback is - called on initial load, unless an Input prop got its value from another initial callback. Callbacks triggered - by user actions typically have one item in triggered, unless the same action changes two props at once or - the callback has several Input props that are all modified by another callback based on a single user action. + Returns a list of all the Input props that changed and caused the callback to execute. It is empty when the + callback is called on initial load, unless an Input prop got its value from another initial callback. + Callbacks triggered by user actions typically have one item in triggered, unless the same action changes + two props at once or the callback has several Input props that are all modified by another callback based on + a single user action. Example: To get the id of the component that triggered the callback: `component_id = ctx.triggered[0]['prop_id'].split('.')[0]` @@ -72,10 +73,11 @@ def triggered(self): @has_context def triggered_prop_ids(self): """ - Returns a dictionary of all the Input props that changed and caused the callback to execute. It is empty when the callback is - called on initial load, unless an Input prop got its value from another initial callback. Callbacks triggered - by user actions typically have one item in triggered, unless the same action changes two props at once or - the callback has several Input props that are all modified by another callback based on a single user action. + Returns a dictionary of all the Input props that changed and caused the callback to execute. It is empty when + the callback is called on initial load, unless an Input prop got its value from another initial callback. + Callbacks triggered by user actions typically have one item in triggered, unless the same action changes + two props at once or the callback has several Input props that are all modified by another callback based + on a single user action. triggered_prop_ids (dict): - keys (str) : the triggered "prop_id" composed of "component_id.component_property" @@ -287,4 +289,4 @@ def using_outputs_grouping(self): callback_context = CallbackContext() -ctx = CallbackContext() + From d7698f1d1ba728e528f411ff85505379f54b1eb5 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Thu, 21 Apr 2022 14:41:58 -0700 Subject: [PATCH 11/13] added changlog and lint --- CHANGELOG.md | 8 ++++++++ dash/_callback_context.py | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb46a1c33d..c1005ccb77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Added +- [#1952](https://github.com/plotly/dash/pull/1952) Improved callback_context + - Closes [#1818](https://github.com/plotly/dash/issues/1818) Closes [#1054](https://github.com/plotly/dash/issues/1054) + - adds `dash.ctx`, a more concise name for `dash.callback_context` + - adds `ctx.triggered_prop_ids`, a dictionary of the component ids and props that triggered the callback. + - adds `ctx.triggered_id`, the `id` of the component that triggered the callback. + - adds `ctx.args_grouping`, a dict of the inputs used with flexible callback signatures. + ### Fixed - [#2015](https://github.com/plotly/dash/pull/2015) Fix bug [#1854](https://github.com/plotly/dash/issues/1854) in which the combination of row_selectable="single or multi" and filter_action="native" caused the JS error. diff --git a/dash/_callback_context.py b/dash/_callback_context.py index bd31784c8f..134723b115 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -287,4 +287,3 @@ def using_outputs_grouping(self): callback_context = CallbackContext() - From 47b7af9c2793328343062cc8d1dd69ea4a3f0164 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Thu, 21 Apr 2022 15:49:00 -0700 Subject: [PATCH 12/13] lint again --- dash/_callback_context.py | 2 +- dash/_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 134723b115..8722f2b6d1 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -154,7 +154,7 @@ def display(btn1, btn2): def update_args_grouping(g): if isinstance(g, dict) and "id" in g: str_id = stringify_id(g["id"]) - prop_id = "{}.{}".format(str_id, g["property"]) + prop_id = f"{str_id}.{g['propery']}" new_values = { "value": g.get("value"), diff --git a/dash/_utils.py b/dash/_utils.py index 7b937b3ff6..aa0470f43d 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -119,7 +119,7 @@ def first(self, *names): value = self.get(name) if value: return value - if names == (): + if not names: return next(iter(self), {}) From ea493c5a1cde06e888638c995821edf4f7e3c125 Mon Sep 17 00:00:00 2001 From: AnnMarieW Date: Thu, 21 Apr 2022 16:58:38 -0700 Subject: [PATCH 13/13] lint again and again --- dash/_callback_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 8722f2b6d1..20f0e887b6 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -154,7 +154,7 @@ def display(btn1, btn2): def update_args_grouping(g): if isinstance(g, dict) and "id" in g: str_id = stringify_id(g["id"]) - prop_id = f"{str_id}.{g['propery']}" + prop_id = f"{str_id}.{g['property']}" new_values = { "value": g.get("value"),