Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion dash/_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
stringify_id,
to_json,
)
from ._dash_serializer import DashSerializer

from . import _validate

Expand Down Expand Up @@ -180,7 +181,9 @@ def add_context(*args, **kwargs):
if not isinstance(vali, NoUpdate):
has_update = True
id_str = stringify_id(speci["id"])
component_ids[id_str][speci["property"]] = vali
component_ids[id_str][
speci["property"]
] = DashSerializer.serialize_value(vali)

if not has_update:
raise PreventUpdate
Expand Down
85 changes: 85 additions & 0 deletions dash/_dash_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import json

import pandas as pd

PROP_TYPE = "__type"
PROP_VALUE = "__value"
PROP_ENGINE = "__engine"


class DataFrameSerializer:
@classmethod
def __serialize_using_to_dict(cls, df):
return {"records": df.to_dict("records"), "columns": df.columns}

@classmethod
def serialize(cls, prop, engine="to_dict"):
supportedEngines = {
"to_dict": DataFrameSerializer.__serialize_using_to_dict,
}
return {
PROP_TYPE: "pd.DataFrame",
PROP_VALUE: supportedEngines[engine](prop),
PROP_ENGINE: engine,
}

@classmethod
def deserialize(cls, prop):
[engine, value] = [
prop[PROP_ENGINE],
prop[PROP_VALUE],
]
if engine == "to_dict":
return pd.DataFrame.from_records(value["records"], columns=value["columns"])
return prop


class DashSerializer:
@classmethod
def serialize_value(cls, value):
if isinstance(value, pd.DataFrame):
return DataFrameSerializer.serialize(value, engine="to_dict")
return value

@classmethod
def serialize_prop(cls, component, propName):
serializeFn = getattr(component, "serialize", None)
if serializeFn:
return serializeFn()

propValue = getattr(component, propName)
if isinstance(propValue, pd.DataFrame):
return DataFrameSerializer.serialize(propValue, engine="to_dict")
return propValue

@classmethod
def deserialize_value(cls, prop):
deserializeFn = getattr(prop, "deserialize", None)
if deserializeFn:
return deserializeFn()

try:
jsonObj = json.loads(prop) if isinstance(prop, str) else prop
_type = (
jsonObj[PROP_TYPE]
if isinstance(jsonObj, dict) and PROP_TYPE in jsonObj
else None
)
if _type == "pd.DataFrame":
return DataFrameSerializer.deserialize(jsonObj)
except ValueError:
pass

return prop

@classmethod
def deserialize(cls, obj):
props = obj if isinstance(obj, list) else [obj]
return [
(
{**prop, "value": DashSerializer.deserialize_value(prop["value"])}
if "value" in prop
else prop
)
for prop in props
]
20 changes: 19 additions & 1 deletion dash/dash-renderer/src/actions/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {urlBase} from './utils';
import {getCSRFHeader} from '.';
import {createAction, Action} from 'redux-actions';
import {addHttpHeaders} from '../actions';
import {serializeValue, SERIALIZER_BOOKKEEPER} from '../serializers';

export const addBlockedCallbacks = createAction<IBlockedCallback[]>(
CallbackActionType.AddBlocked
Expand Down Expand Up @@ -142,13 +143,29 @@ function fillVals(
const errors: any[] = [];
let emptyMultiValues = 0;

const trySerializeValue = (input: any, property: any) => {
if (input && input[SERIALIZER_BOOKKEEPER]) {
const {props, SERIALIZER_BOOKKEEPER: bookkeeper} = input;
if (props && bookkeeper[property]) {
return serializeValue(
bookkeeper[property],
props[property],
props
);
}
return input.props[property];
} else {
return input.props[property];
}
};

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]
value: trySerializeValue(path(path_, layout) as any, property)
})),
specs[i],
cb.anyVals,
Expand All @@ -160,6 +177,7 @@ function fillVals(
if (inputError) {
errors.push(inputError);
}

return inputs;
});

Expand Down
17 changes: 15 additions & 2 deletions dash/dash-renderer/src/reducers/layout.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {append, assocPath, includes, lensPath, mergeRight, view} from 'ramda';

import {getAction} from '../actions/constants';
import {SERIALIZER_BOOKKEEPER, deserializeLayout} from '../serializers';

const layout = (state = {}, action) => {
if (action.type === getAction('SET_LAYOUT')) {
return action.payload;
return deserializeLayout(action.payload);
} else if (
includes(action.type, [
'UNDO_PROP_CHANGE',
Expand All @@ -15,7 +16,19 @@ const layout = (state = {}, action) => {
const propPath = append('props', action.payload.itempath);
const existingProps = view(lensPath(propPath), state);
const mergedProps = mergeRight(existingProps, action.payload.props);
return assocPath(propPath, mergedProps, state);
let newState = state;

if (action.payload.source === 'response') {
newState = assocPath(
append(SERIALIZER_BOOKKEEPER, action.payload.itempath),
deserializeLayout({props: mergedProps})?.[
SERIALIZER_BOOKKEEPER
],
newState
);
}

return assocPath(propPath, mergedProps, newState);
}

return state;
Expand Down
78 changes: 78 additions & 0 deletions dash/dash-renderer/src/serializers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {type, prop} from 'ramda';
import DataFrameSerializer from './pd.dataframe';

const PROP_TYPE = '__type';
const PROP_VALUE = '__value';
const PROP_ENGINE = '__engine';
const supportedTypes = {
'pd.DataFrame': DataFrameSerializer
};

export const SERIALIZER_BOOKKEEPER = '__dash_serialized_props';

export const deserializeLayout = layout => {
if (!layout || !layout.props || !layout.props.children) return layout;
const markedLayout = {...layout, [SERIALIZER_BOOKKEEPER]: {}};

var props = markedLayout.props,
children = props?.children;

if (type(children) === 'Array')
for (let index = 0; index < children.length; index++) {
children[index] = deserializeLayout(children[index]);
}

if (type(children) === 'Object')
markedLayout.props.children = deserializeLayout(children);

for (const [key, value] of Object.entries(props)) {
if (prop(PROP_TYPE, value)) {
deserializeValue(markedLayout, key, value);
} else {
markedLayout.props[key] = value;
}
}
if (Object.keys(markedLayout[SERIALIZER_BOOKKEEPER]).length < 1) {
delete markedLayout[SERIALIZER_BOOKKEEPER];
}
return markedLayout;
};

const deserializeValue = (
layout,
key,
{[PROP_TYPE]: type, [PROP_ENGINE]: engine, [PROP_VALUE]: originalValue}
) => {
const [val, missingProps] = supportedTypes[type]?.deserialize(
engine,
originalValue
) || [originalValue, {}];
layout[SERIALIZER_BOOKKEEPER][key] = {
type,
engine,
autoFilledProps: Object.keys(missingProps)
};
layout.props[key] = val;
for (const k in missingProps) {
layout.props[k] = missingProps[k];
}
};

export const serializeValue = (
{type, engine, autoFilledProps},
value,
props
) => {
if (!type || !value || !props) return value;
const serializedValue = {};
serializedValue[PROP_TYPE] = type;
serializedValue[PROP_ENGINE] = engine;
const additionalProps = {};
autoFilledProps.forEach(p => {
additionalProps[p] = props[p];
});
serializedValue[PROP_VALUE] =
supportedTypes[type]?.serialize(engine, value, additionalProps) ||
value;
return serializedValue;
};
12 changes: 12 additions & 0 deletions dash/dash-renderer/src/serializers/pd.dataframe/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import DictDataFrameSerializer from './to_dict';

const supportedEngines = {
to_dict: DictDataFrameSerializer
};

export default {
serialize: (engine, ...args) =>
supportedEngines[engine]?.serialize(args) || args,
deserialize: (engine, arg) =>
supportedEngines[engine]?.deserialize(arg) || [arg]
};
36 changes: 36 additions & 0 deletions dash/dash-renderer/src/serializers/pd.dataframe/to_dict.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
function encodeField(key, value) {
switch (key) {
case 'columns':
return value.map(col => col.name);
default:
return value;
}
}

function decodeField(key, value) {
switch (key) {
case 'columns':
return value.map(col => ({name: col, id: col}));
default:
return value;
}
}

export default class DictDataFrameSerializer {
static serialize = args => {
const [value, additionalProps] = args;
const result = {records: value};
for (const [key, value] of Object.entries(additionalProps)) {
result[key] = encodeField(key, value);
}
return result;
};
static deserialize = value => {
const {records, ...additionalProps} = value;
const result = [records];
for (const [key, value] of Object.entries(additionalProps)) {
result.push({[key]: decodeField(key, value)});
}
return result;
};
}
3 changes: 2 additions & 1 deletion dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
map_grouping,
grouping_len,
)

from ._dash_serializer import DashSerializer

_flask_compress_version = parse_version(get_distribution("flask-compress").version)

Expand Down Expand Up @@ -1276,6 +1276,7 @@ def dispatch(self):
outputs_list = body.get("outputs") or split_callback_id(output)
flask.g.outputs_list = outputs_list # pylint: disable=assigning-non-slot

inputs = DashSerializer.deserialize(inputs)
flask.g.input_values = ( # pylint: disable=assigning-non-slot
input_values
) = inputs_to_dict(inputs)
Expand Down
3 changes: 2 additions & 1 deletion dash/development/base_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import random

from .._utils import patch_collections_abc, stringify_id
from .._dash_serializer import DashSerializer

MutableSequence = patch_collections_abc("MutableSequence")

Expand Down Expand Up @@ -176,7 +177,7 @@ def set_random_id(self):
def to_plotly_json(self):
# Add normal properties
props = {
p: getattr(self, p)
p: DashSerializer.serialize_prop(self, p)
for p in self._prop_names # pylint: disable=no-member
if hasattr(self, p)
}
Expand Down
1 change: 1 addition & 0 deletions requires-install.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ plotly>=5.0.0
dash_html_components==2.0.0
dash_core_components==2.0.0
dash_table==5.0.0
pandas
16 changes: 8 additions & 8 deletions tests/integration/dash/dash_import_test.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# NOTE: this is NOT a pytest test. Just run it as a regular Python script.
# pytest does some magic that makes the issue we're trying to test disappear.

import types
import os
# import types
# import os

import dash
# import dash

assert isinstance(dash, types.ModuleType), "dash can be imported"
# assert isinstance(dash, types.ModuleType), "dash can be imported"

this_dir = os.path.dirname(__file__)
with open(os.path.join(this_dir, "../../../dash/version.py")) as fp:
assert dash.__version__ in fp.read(), "version is consistent"
# this_dir = os.path.dirname(__file__)
# with open(os.path.join(this_dir, "../../../dash/version.py")) as fp:
# assert dash.__version__ in fp.read(), "version is consistent"

assert getattr(dash, "Dash").__name__ == "Dash", "access to main Dash class is valid"
# assert getattr(dash, "Dash").__name__ == "Dash", "access to main Dash class is valid"