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

Validate component properties #264 #340

Closed
wants to merge 96 commits into from
Closed

Conversation

rmarren1
Copy link
Contributor

@rmarren1 rmarren1 commented Aug 17, 2018

PR for #264

Current Prerelease

pip install dash==0.29.0rc8
pip install dash-renderer==0.15.0rc1
pip install dash-core-components==0.31.0rc2
pip install dash-html-components==0.14.0rc3

Validation is on by default. To turn it off, you can set app.config.disable_component_validation = True

Test Cases to run for demo

import dash
import dash_html_components as html
import dash_core_components as dcc
import plotly.graph_objs as go
from dash.dependencies import Input, Output

app = dash.Dash()
app.scripts.config.serve_locally=True

app.layout = html.Div([
    html.Button(id='click1', children='click to return bad Div children'),
    html.Div(id='output1', **{'data-cb': 'foo'}),
    html.Button(id='click2', children='click to return a bad figure'),
    dcc.Graph(id='output2', figure={'data': [], 'layout': {}}),
    html.Button(id='click3', children='click to return a bad radio'),
    dcc.RadioItems(id='output3', options=[{'value': 'okay', 'label': 'okay'}]),
    html.Button(id='click4', children='click to make a figure with no id'),
    html.Div(id='output4'),
])



@app.callback(Output('output1', 'children'),
              [Input('click1', 'n_clicks')])
def crash_it1(clicks):
    if clicks:
        return [[]]
    return clicks

@app.callback(Output('output2', 'figure'),
              [Input('click2', 'n_clicks')])
def crash_it2(clicks):
    if clicks:
        return {'data': {'x': [1, 2, 3], 'y': [1, 2, 3], 'type': 'scatter'}, 'layout': {}}
    return go.Figure(data=[go.Scatter(x=[1,2,3], y=[1,2,3])], layout=go.Layout()) 

@app.callback(Output('output3', 'options'),
              [Input('click3', 'n_clicks')])
def crash_it3(clicks):
    if clicks:
        return [{'value': {'not okay': True}, 'labl': 'not okay'}]
    return [{'value': 'okay', 'label': 'okay'}]

@app.callback(Output('output4', 'children'),
              [Input('click4', 'n_clicks')])
def crash_it4(clicks):
    if clicks:
        return dcc.Graph()
    return dcc.Graph(id='hi')

app.run_server(debug=True, port=8050)

Example Error Messages

CallbackOutputValidationError: 


A Dash Callback produced an invalid value!

Dash tried to update the `figure` prop of the
`Graph` with id `output2` by calling the
`crash_it2` function with `(1)` as arguments.

This function call returned `{'layout': {}, 'data': {'y': [1, 2, 3], 'x': [1, 2, 3], 'type': 'scatter'}}`, which did not pass
validation tests for the `Graph` component.

The expected schema for the `figure` prop of the
`Graph` component is:

***************************************************************
{'validator': 'plotly_figure'}
***************************************************************

The errors in validation are as follows:

* figure	<- Invalid Plotly Figure:


    Invalid value of type '__builtin__.dict' received for the 'data' property of 
        Received value: {'y': [1, 2, 3], 'x': [1, 2, 3], 'type': 'scatter'}

    The 'data' property is a tuple of trace instances
    that may be specified as:
      - A list or tuple of trace instances
        (e.g. [Scatter(...), Bar(...)])
      - A list or tuple of dicts of string/value properties where:
        - The 'type' property specifies the trace type
            One of: ['mesh3d', 'splom', 'scattercarpet',
                     'scattergl', 'scatterternary', 'pie',
                     'surface', 'histogram', 'ohlc', 'heatmapgl',
                     'cone', 'scatterpolar', 'table',
                     'scatterpolargl', 'histogram2d', 'contour',
                     'carpet', 'box', 'violin', 'bar',
                     'contourcarpet', 'area', 'choropleth',
                     'candlestick', 'streamtube', 'parcoords',
                     'heatmap', 'barpolar', 'scattermapbox',
                     'scatter3d', 'pointcloud',
                     'histogram2dcontour', 'scatter', 'scattergeo',
                     'sankey']

        - All remaining properties are passed to the constructor of
          the specified trace type

        (e.g. [{'type': 'scatter', ...}, {'type': 'bar, ...}])

CallbackOutputValidationError: 


A Dash Callback produced an invalid value!

Dash tried to update the `options` prop of the
`RadioItems` with id `output3` by calling the
`crash_it3` function with `(1)` as arguments.

This function call returned `[{'value': {'not okay': True}, 'labl': 'not okay'}]`, which did not pass
validation tests for the `RadioItems` component.

The expected schema for the `options` prop of the
`RadioItems` component is:

***************************************************************
{'schema': {'allow_unknown': False,
            'nullable': False,
            'schema': {'disabled': {'type': 'boolean'},
                       'label': {'type': 'string'},
                       'value': {'type': 'string'}},
            'type': 'dict'},
 'type': 'list'}
***************************************************************

The errors in validation are as follows:

* options
 * 0
  * value	<- must be of string type
  * labl	<- unknown field

ComponentInitializationValidationError: 


A Dash Component was initialized with invalid properties!

Dash tried to create a `RadioItems` component with the
following arguments, which caused a validation failure:

***************************************************************
{'id': 'output3', 'options': [{'label': 'okay', 'value': {}}]}
***************************************************************

The expected schema for the `RadioItems` component is:

***************************************************************
{'className': {'type': 'string'},
 'dashEvents': {'allowed': ['change'], 'type': ('string', 'number')},
 'fireEvent': {},
 'id': {'type': 'string'},
 'inputClassName': {'type': 'string'},
 'inputStyle': {'type': 'dict'},
 'labelClassName': {'type': 'string'},
 'labelStyle': {'type': 'dict'},
 'options': {'schema': {'allow_unknown': False,
                        'nullable': False,
                        'schema': {'disabled': {'type': 'boolean'},
                                   'label': {'type': 'string'},
                                   'value': {'type': 'string'}},
                        'type': 'dict'},
             'type': 'list'},
 'setProps': {},
 'style': {'type': 'dict'},
 'value': {'type': 'string'}}
***************************************************************

The errors in validation are as follows:


* options
 * 0
  * value	<- must be of string type

PropTypes to Cerberus reference

PropType Cerberus Schema Validated Against Known Current Limitations
array {'type': 'list'}
bool {'type': 'boolean'}
func {} No validation occurs.
object {'type': 'dict'} Validates that input is explicitly a dict object. We cannot be more general (e.g. collections.abc.Mapping) since the core Component is an instance of that.
string {'type': 'string'}
symbol {} No validation occurs
node {'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}]}}]}
instanceOf(Object) {} No validation occurs
oneOf(['val1', 2]) {'allowed': [None, 'val1', 2]} Strings will have ' characters stripped off each end. This is because metadata.json generation serializes the literal values as json, so for example PropTypes.oneOf(['News', 'Photos']) serializes to ["'News'", "'Photos'"]. reactjs/react-docgen#57
oneOfType( [ PropTypes.string, PropTypes.bool ] ) {'anyof': [{'type': 'string', 'type': 'boolean'}]} If one of the types is a PropType that cannot be validated individually (e.g. PropType.func), no validation will occur and the schema will effectively be {}
arrayOf( PropTypes.number ) {'type': 'list', 'schema': {'type': 'number'}}
objectOf( PropTypes.number ) {'type': 'dict', 'valueschema': {'type': 'number'}}
shape( { k1: PropTypes.string, k2: PropTypes.number } ) {'type': 'dict', 'allow_unknown': False, 'schema': {'k1': {'type': 'string}, 'k2': {'type': 'number'}}}
any {'type': ('boolean', 'number', 'string', 'dict', 'list')}

dash/dash.py Outdated
@@ -805,6 +811,23 @@ def wrap_func(func):
def add_context(*args, **kwargs):

output_value = func(*args, **kwargs)
setattr(

This comment was marked as outdated.

dash/dash.py Outdated
self._components[output.component_id]._validator.validate({
output.component_property: output_value
})
if output.component_property == 'children':

This comment was marked as outdated.

dash/dash.py Outdated
break
matched_state_value = matched_state.get('value', None)
args.append(matched_state_value)
setattr(

This comment was marked as outdated.

pass


cerberus.Validator.types_mapping['component'] =\
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let us validate Component objects with cerberus (useful for children prop)

@@ -382,7 +488,8 @@ def __repr__(self):
p not in keyword.kwlist and
p not in ['dashEvents', 'fireEvent', 'setProps']] + ['**kwargs']
)

schema = {str(k): generate_property_schema(v)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make cerberus schema for this dash component type.

@Bachibouzouk
Copy link

Hi, I tried to pip install this branch locally in a venv to test this PR, but I the way I did it doesn't work as I get an error by running the example code you provide above :
AttributeError: 'RadioItems' object has no attribute '_schema'

I typed the command pip install git+https://github.com/rmarren1/dash.git@validate, maybe it is not the way to proceed?




# Forward declated Component class, see below.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you mean 'declared'? PEP8 requires one space less :)

PS : this is my first review on dash code and I am mainly reading the code you wrote to learn more, my suggestion will mostly be about styling and spelling, I apologize in advance for that!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my suggestion will mostly be about styling and spelling, I apologize in advance for that!

Note that we enforce PEP8 in our tests here:

pylint dash setup.py --rcfile=$PYLINTRC
flake8 dash setup.py

So, it's not necessary to review PEP8 issues in our PRs as they will get fixed before it gets merged into master.

@chriddyp
Copy link
Member

chriddyp commented Aug 20, 2018

To validate the components in the callbacks, I think we'll need to pass the component name back in the _dash-update-components API call. With Dash's stateless backend, I think this will be the most reliable way to ensure that the front-end component that is getting updated is the same one that we are validating against.

So, that is, we could modify the Dash API to include type and namespace in the payload:

https://github.com/plotly/dash-renderer/blob/0dc6f331eda404ee848145db71a4dc3bc35a759e/src/actions/index.js#L406-L408

and then do a component lookup in the callback decorator

@rmarren1
Copy link
Contributor Author

^ Definitely, I was going to add this to the next iteration. You don't need this to validate against components in the initial layout, but I don't think there is any other way to validate against dynamically generated components without updating state in the backend after initialization.

@rmarren1
Copy link
Contributor Author

@rmarren1
Copy link
Contributor Author

rmarren1 commented Aug 21, 2018

PR which adds 'type' and 'namespace' to the _dash-update-components payload. plotly/dash-renderer#69

@rmarren1
Copy link
Contributor Author

suppress_callback_exceptions is the current name, I would just be changing the name of the config to disable validation from disable_component_validation to suppress_validation_exceptions.

@T4rk1n
Copy link
Contributor

T4rk1n commented Oct 18, 2018

Oh ok, I would still integrate with the dev tools so just setting debug=True is required for it.

@rmarren1
Copy link
Contributor Author

rmarren1 commented Oct 18, 2018

Oh I thought you meant something different, yeah it should be disabled in production.

@T4rk1n
Copy link
Contributor

T4rk1n commented Oct 30, 2018

@rmarren1 How would the component as prop validate ? proptype is PropTypes.node.

@rmarren1
Copy link
Contributor Author

@T4rk1n There is a custom type called component which makes sure objects are an instance of dash.development.base_component.Component, PropTypes.node works with this custom type:

'node': lambda x: {
'anyof': [
{'type': 'component'},
{'type': 'boolean'},
{'type': 'number'},
{'type': 'string'},
{
'type': 'list',
'schema': {
'type': (
'component',
'boolean',
'number',
'string')
}
}
]
},

@rmarren1
Copy link
Contributor Author

rmarren1 commented Nov 8, 2018

Closing in favor of #452, which is the same PR but with a more readable commit history. Will link back to here for the relevant discussions.

@rmarren1 rmarren1 closed this Nov 8, 2018
HammadTheOne pushed a commit to HammadTheOne/dash that referenced this pull request May 28, 2021
AnnMarieW pushed a commit to AnnMarieW/dash that referenced this pull request Jan 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants