From 63d7d8a0d0ddd29d8485c0b81bac4e79c4468988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Marko=CC=81?= Date: Fri, 24 Sep 2021 20:40:06 +0200 Subject: [PATCH 1/8] Keep track of component property types or properties passed as {__value: 'value', __type: 'datatype'} --- dash/dash-renderer/src/actions/callbacks.ts | 22 +++++-- dash/dash-renderer/src/reducers/layout.js | 63 ++++++++++++++++++++- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 2769d4c5eb..0d0af1dd21 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -145,11 +145,23 @@ function fillVals( const inputVals = getter(paths).map((inputList: any, i: number) => { const [inputs, inputError] = unwrapIfNotMulti( paths, - inputList.map(({id, property, path: path_}: any) => ({ - id, - property, - value: (path(path_, layout) as any).props[property] - })), + inputList.map(({id, property, path: path_}: any) => { + const type = (path(path_, layout) as any).propertyTypes?.[property] + let value = (path(path_, layout) as any).props[property] + + if (type) { + value = { + __type: type, + __value: value + } + } +console.log('type', type) + return ({ + id, + property, + value + }); + }), specs[i], cb.anyVals, depType diff --git a/dash/dash-renderer/src/reducers/layout.js b/dash/dash-renderer/src/reducers/layout.js index 6c082f313c..fe128417bf 100644 --- a/dash/dash-renderer/src/reducers/layout.js +++ b/dash/dash-renderer/src/reducers/layout.js @@ -1,10 +1,38 @@ -import {append, assocPath, includes, lensPath, mergeRight, view} from 'ramda'; +import { + append, + assocPath, + includes, + lensPath, + mergeRight, + view, + type +} from 'ramda'; import {getAction} from '../actions/constants'; +const processProperties = node => { + const updatedNode = {...node, propertyTypes: {}}; + const { + props, + props: {children} + } = node; + + if (type(children) === 'Array') { + updatedNode.props.children = children.map(child => + processProperties(child) + ); + } + Object.entries(props).forEach(([key, value]) => { + updatedNode.props[key] = value?.__value || value; + updatedNode.propertyTypes[key] = value?.__type || null; + }); + + return updatedNode; +}; + const layout = (state = {}, action) => { if (action.type === getAction('SET_LAYOUT')) { - return action.payload; + return processProperties(action.payload); } else if ( includes(action.type, [ 'UNDO_PROP_CHANGE', @@ -13,9 +41,38 @@ const layout = (state = {}, action) => { ]) ) { const propPath = append('props', action.payload.itempath); + const propertyTypesPath = append( + 'propertyTypes', + action.payload.itempath + ); + const existingProps = view(lensPath(propPath), state); const mergedProps = mergeRight(existingProps, action.payload.props); - return assocPath(propPath, mergedProps, state); + + const propertyValues = Object.entries(mergedProps).reduce( + (a, [key, value]) => { + return {...a, [key]: value.__value || value}; + }, + {} + ); + + const propertyTypes = Object.entries(mergedProps).reduce( + (a, [key, value]) => { + return {...a, [key]: value.__type || null}; + }, + {} + ); + + let updatedState = assocPath(propPath, propertyValues, state); + if (action.payload.source === 'response') { + updatedState = assocPath( + propertyTypesPath, + propertyTypes, + updatedState + ); + } + + return updatedState; } return state; From 15f233d6179f1873ae0f53de5830ba979d035105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Marko=CC=81?= Date: Sat, 25 Sep 2021 20:29:22 +0200 Subject: [PATCH 2/8] Serialize/Unserialize Python data structures from/to Dash-renderer JSON objects --- dash/_utils.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dash/_utils.py b/dash/_utils.py index 1c147065c4..6b85421f0e 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -9,6 +9,7 @@ import io import json from functools import wraps +import pandas as pd from . import exceptions logger = logging.getLogger() @@ -247,3 +248,19 @@ def _wrapper(*args, **kwargs): return _wrapper return wrapper + + +class serializer: + @classmethod + def serialize(cls, obj): + if isinstance(obj, pd.DataFrame): + return {"__type": "DataFrame", "__value": obj.to_dict("records")} + + return {"__type": "JSON", "__value": obj} + + @classmethod + def unserialize(cls, obj): + if obj["__type"] == "DataFrame": + return pd.DataFrame(obj["__value"]) + + return obj["__value"] From 62cfdf9d1e7b31079a28f9254ad702995a121f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Marko=CC=81?= Date: Mon, 27 Sep 2021 13:05:49 +0200 Subject: [PATCH 3/8] Move serializer to dash core making possible to accept complex data types (currently Pandas DataFrame) as Layout properties --- dash/_utils.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/dash/_utils.py b/dash/_utils.py index 6b85421f0e..69191a615b 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -19,7 +19,7 @@ def to_json(value): # pylint: disable=import-outside-toplevel from plotly.io.json import to_json_plotly - return to_json_plotly(value) + return to_json_plotly(serializer.serialize_tree(value)) def interpolate_str(template, **data): @@ -252,15 +252,37 @@ def _wrapper(*args, **kwargs): class serializer: @classmethod - def serialize(cls, obj): - if isinstance(obj, pd.DataFrame): - return {"__type": "DataFrame", "__value": obj.to_dict("records")} + def serialize(cls, prop): + if isinstance(prop, pd.DataFrame): + return {"__type": "DataFrame", "__value": prop.to_dict("records")} - return {"__type": "JSON", "__value": obj} + return prop @classmethod - def unserialize(cls, obj): - if obj["__type"] == "DataFrame": - return pd.DataFrame(obj["__value"]) + def unserialize(cls, prop): + if prop["__type"] == "DataFrame": + return pd.DataFrame(prop["__value"]) + + return prop + + @classmethod + def serialize_tree(cls, obj): + if isinstance(obj, pd.DataFrame): + return cls.serialize(obj) + + # Plotly + try: + obj = obj.to_plotly_json() + except AttributeError: + pass + + if isinstance(obj, (list, tuple)): + if obj: + # Must process list recursively even though it may be slow + return [cls.serialize_tree(v) for v in obj] + + # Recurse into lists and dictionaries + if isinstance(obj, dict): + return {k: cls.serialize_tree(v) for k, v in obj.items()} - return obj["__value"] + return obj From 59035706c52a44c0abfec7361859b067053fc4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Marko=CC=81?= Date: Mon, 27 Sep 2021 16:08:03 +0200 Subject: [PATCH 4/8] Fix Linting Error --- dash/dash-renderer/src/actions/callbacks.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 0d0af1dd21..0997400c60 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -146,21 +146,23 @@ function fillVals( const [inputs, inputError] = unwrapIfNotMulti( paths, inputList.map(({id, property, path: path_}: any) => { - const type = (path(path_, layout) as any).propertyTypes?.[property] - let value = (path(path_, layout) as any).props[property] + const type = (path(path_, layout) as any).propertyTypes?.[ + property + ]; + let value = (path(path_, layout) as any).props[property]; if (type) { value = { __type: type, __value: value - } + }; } -console.log('type', type) - return ({ + + return { id, property, value - }); + }; }), specs[i], cb.anyVals, From 7e5ce6f97c64f1af8ee714b457d37163ca9a9729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Marko=CC=81?= Date: Tue, 28 Sep 2021 15:37:10 +0200 Subject: [PATCH 5/8] Fix PropertyType processing recursion in Layout reducer and apply it for PROP_CHANGE (callback) actions too --- dash/dash-renderer/src/reducers/layout.js | 33 ++++++----------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/dash/dash-renderer/src/reducers/layout.js b/dash/dash-renderer/src/reducers/layout.js index fe128417bf..c0cbe34fad 100644 --- a/dash/dash-renderer/src/reducers/layout.js +++ b/dash/dash-renderer/src/reducers/layout.js @@ -21,7 +21,10 @@ const processProperties = node => { updatedNode.props.children = children.map(child => processProperties(child) ); + } else if (type(children) === 'Object' && !children.__type) { + updatedNode.props.children = processProperties(children); } + Object.entries(props).forEach(([key, value]) => { updatedNode.props[key] = value?.__value || value; updatedNode.propertyTypes[key] = value?.__type || null; @@ -32,7 +35,9 @@ const processProperties = node => { const layout = (state = {}, action) => { if (action.type === getAction('SET_LAYOUT')) { - return processProperties(action.payload); + const updatedState = processProperties(action.payload); + + return updatedState; } else if ( includes(action.type, [ 'UNDO_PROP_CHANGE', @@ -41,35 +46,13 @@ const layout = (state = {}, action) => { ]) ) { const propPath = append('props', action.payload.itempath); - const propertyTypesPath = append( - 'propertyTypes', - action.payload.itempath - ); const existingProps = view(lensPath(propPath), state); const mergedProps = mergeRight(existingProps, action.payload.props); - const propertyValues = Object.entries(mergedProps).reduce( - (a, [key, value]) => { - return {...a, [key]: value.__value || value}; - }, - {} - ); - - const propertyTypes = Object.entries(mergedProps).reduce( - (a, [key, value]) => { - return {...a, [key]: value.__type || null}; - }, - {} - ); - - let updatedState = assocPath(propPath, propertyValues, state); + let updatedState = assocPath(propPath, mergedProps, state); if (action.payload.source === 'response') { - updatedState = assocPath( - propertyTypesPath, - propertyTypes, - updatedState - ); + updatedState = processProperties(updatedState); } return updatedState; From 9b98e537267655b545d6ed92bec1903ed7086aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Marko=CC=81?= Date: Tue, 28 Sep 2021 15:40:51 +0200 Subject: [PATCH 6/8] Allow general Property Type in unserialization method --- dash/_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dash/_utils.py b/dash/_utils.py index 69191a615b..180c6d42f9 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -263,6 +263,9 @@ def unserialize(cls, prop): if prop["__type"] == "DataFrame": return pd.DataFrame(prop["__value"]) + if prop["__type"]: + return prop["__value"] + return prop @classmethod From 7b91be4deb699233a3cb84f13a9e7f576329a607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Marko=CC=81?= Date: Wed, 29 Sep 2021 11:58:26 +0200 Subject: [PATCH 7/8] Fix: avoid losing exiting propertyTypes --- dash/dash-renderer/src/reducers/layout.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dash/dash-renderer/src/reducers/layout.js b/dash/dash-renderer/src/reducers/layout.js index c0cbe34fad..c5056a35de 100644 --- a/dash/dash-renderer/src/reducers/layout.js +++ b/dash/dash-renderer/src/reducers/layout.js @@ -11,7 +11,7 @@ import { import {getAction} from '../actions/constants'; const processProperties = node => { - const updatedNode = {...node, propertyTypes: {}}; + const updatedNode = {...node, propertyTypes: {...node.propertyTypes}}; const { props, props: {children} @@ -27,7 +27,8 @@ const processProperties = node => { Object.entries(props).forEach(([key, value]) => { updatedNode.props[key] = value?.__value || value; - updatedNode.propertyTypes[key] = value?.__type || null; + updatedNode.propertyTypes[key] = + value?.__type || updatedNode.propertyTypes[key] || null; }); return updatedNode; From aa629240f5c0d72d09d3dd46eedfaca3ebd14e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Marko=CC=81?= Date: Wed, 29 Sep 2021 13:52:39 +0200 Subject: [PATCH 8/8] Unserialize callback inputs --- dash/_utils.py | 26 ++++++++++++++++---------- dash/dash.py | 4 ++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/dash/_utils.py b/dash/_utils.py index 180c6d42f9..e496357cae 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -258,16 +258,6 @@ def serialize(cls, prop): return prop - @classmethod - def unserialize(cls, prop): - if prop["__type"] == "DataFrame": - return pd.DataFrame(prop["__value"]) - - if prop["__type"]: - return prop["__value"] - - return prop - @classmethod def serialize_tree(cls, obj): if isinstance(obj, pd.DataFrame): @@ -289,3 +279,19 @@ def serialize_tree(cls, obj): return {k: cls.serialize_tree(v) for k, v in obj.items()} return obj + + @classmethod + def unserialize(cls, prop): + if not (isinstance(prop, dict) and "__type" in prop): + return prop + if prop["__type"] == "DataFrame": + return pd.DataFrame(prop["__value"]) + + return prop["__value"] + + @classmethod + def unserialize_input(cls, cb_input): + if "value" in cb_input: + return {**cb_input, "value": cls.unserialize(cb_input["value"])} + + return cb_input diff --git a/dash/dash.py b/dash/dash.py index b567e5b9d9..196a63e3ba 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -51,6 +51,7 @@ split_callback_id, strip_relative_path, to_json, + serializer, ) from . import _callback from . import _dash_renderer @@ -1266,9 +1267,12 @@ 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", [] ) + inputs = [serializer.unserialize_input(v) for v in inputs] + flask.g.states_list = state = body.get( # pylint: disable=assigning-non-slot "state", [] )