-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Validate component properties #264 - March 1 #452
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
Changes from 24 commits
5ac225b
b1ef131
9e5c370
26e425a
10498d2
d943e2e
b12c73d
c123915
6381b54
7ac65f2
1ba24f7
f21372b
398950d
83f70d7
0883290
422c2e2
3ff6dd4
b81084a
27d7da5
7aa7060
28b8a13
ee12470
e817d75
6b8cfd3
2c2faf5
5d560e6
1f48149
29c363c
783d2c2
8e96ed2
79b5251
1bf1036
d842651
5936dfa
dcc3c40
06d03c7
83f501a
1dae812
7d00b80
86d5e7c
f7312bf
5a50ee8
792ef39
598a432
aa49488
56c2873
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,10 @@ tox | |
tox-pyenv | ||
mock | ||
six | ||
numpy | ||
pandas | ||
plotly>=2.0.8 | ||
requests[security] | ||
flake8 | ||
pylint==1.9.2 | ||
Cerberus |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,8 +8,10 @@ | |
import pkgutil | ||
import warnings | ||
import re | ||
import pprint | ||
|
||
from functools import wraps | ||
from textwrap import dedent | ||
|
||
import plotly | ||
import dash_renderer | ||
|
@@ -20,6 +22,8 @@ | |
from .dependencies import Event, Input, Output, State | ||
from .resources import Scripts, Css | ||
from .development.base_component import Component | ||
from .development.validator import (DashValidator, | ||
generate_validation_error_message) | ||
from . import exceptions | ||
from ._utils import AttributeDict as _AttributeDict | ||
from ._utils import interpolate_str as _interpolate | ||
|
@@ -84,6 +88,7 @@ def __init__( | |
external_scripts=None, | ||
external_stylesheets=None, | ||
suppress_callback_exceptions=None, | ||
suppress_validation_exceptions=None, | ||
components_cache_max_age=None, | ||
**kwargs): | ||
|
||
|
@@ -126,6 +131,10 @@ def __init__( | |
'suppress_callback_exceptions', | ||
suppress_callback_exceptions, env_configs, False | ||
), | ||
'suppress_validation_exceptions': _configs.get_config( | ||
'suppress_validation_exceptions', | ||
suppress_validation_exceptions, env_configs, False | ||
), | ||
'routes_pathname_prefix': routes_pathname_prefix, | ||
'requests_pathname_prefix': requests_pathname_prefix, | ||
'include_assets_files': _configs.get_config( | ||
|
@@ -572,7 +581,7 @@ def react(self, *args, **kwargs): | |
'Use `callback` instead. `callback` has a new syntax too, ' | ||
'so make sure to call `help(app.callback)` to learn more.') | ||
|
||
def _validate_callback(self, output, inputs, state, events): | ||
def _validate_callback_definition(self, output, inputs, state, events): | ||
# pylint: disable=too-many-branches | ||
layout = self._cached_layout or self._layout_value() | ||
|
||
|
@@ -710,7 +719,7 @@ def _validate_callback(self, output, inputs, state, events): | |
output.component_id, | ||
output.component_property).replace(' ', '')) | ||
|
||
def _validate_callback_output(self, output_value, output): | ||
def _debug_callback_serialization_error(self, output_value, output): | ||
valid = [str, dict, int, float, type(None), Component] | ||
|
||
def _raise_invalid(bad_val, outer_val, bad_type, path, index=None, | ||
|
@@ -828,7 +837,7 @@ def _validate_value(val, index=None): | |
# relationships | ||
# pylint: disable=dangerous-default-value | ||
def callback(self, output, inputs=[], state=[], events=[]): | ||
self._validate_callback(output, inputs, state, events) | ||
self._validate_callback_definition(output, inputs, state, events) | ||
|
||
callback_id = '{}.{}'.format( | ||
output.component_id, output.component_property | ||
|
@@ -850,13 +859,11 @@ def callback(self, output, inputs=[], state=[], events=[]): | |
|
||
def wrap_func(func): | ||
@wraps(func) | ||
def add_context(*args, **kwargs): | ||
|
||
output_value = func(*args, **kwargs) | ||
def add_context(validated_output): | ||
response = { | ||
'response': { | ||
'props': { | ||
output.component_property: output_value | ||
output.component_property: validated_output | ||
} | ||
} | ||
} | ||
|
@@ -867,7 +874,10 @@ def add_context(*args, **kwargs): | |
cls=plotly.utils.PlotlyJSONEncoder | ||
) | ||
except TypeError: | ||
self._validate_callback_output(output_value, output) | ||
self._debug_callback_serialization_error( | ||
validated_output, | ||
output | ||
) | ||
raise exceptions.InvalidCallbackReturnValue(''' | ||
The callback for property `{property:s}` | ||
of component `{id:s}` returned a value | ||
|
@@ -884,6 +894,7 @@ def add_context(*args, **kwargs): | |
mimetype='application/json' | ||
) | ||
|
||
self.callback_map[callback_id]['func'] = func | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do that in two difference part ? Couldn't your additions in dispatch be in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We would have to pass |
||
self.callback_map[callback_id]['callback'] = add_context | ||
|
||
return add_context | ||
|
@@ -912,7 +923,85 @@ def dispatch(self): | |
c['id'] == component_registration['id'] | ||
][0]) | ||
|
||
return self.callback_map[target_id]['callback'](*args) | ||
output_value = self.callback_map[target_id]['func'](*args) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The old call to the functions also had There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it did, this was the last change to that line and it had |
||
|
||
# Only validate if we get required information from renderer | ||
# and validation is not turned off by user | ||
if ( | ||
not self.config.suppress_validation_exceptions and | ||
'namespace' in output and | ||
'type' in output | ||
): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would fit without the ()
|
||
# Python2.7 might make these keys and values unicode | ||
namespace = str(output['namespace']) | ||
component_type = str(output['type']) | ||
component_id = str(output['id']) | ||
component_property = str(output['property']) | ||
callback_func_name = self.callback_map[target_id]['func'].__name__ | ||
self._validate_callback_output(namespace, component_type, | ||
component_id, component_property, | ||
callback_func_name, | ||
args, output_value) | ||
|
||
return self.callback_map[target_id]['callback'](output_value) | ||
|
||
def _validate_callback_output(self, namespace, component_type, | ||
component_id, component_property, | ||
callback_func_name, args, value): | ||
module = sys.modules[namespace] | ||
component = getattr(module, component_type) | ||
# pylint: disable=protected-access | ||
validator = DashValidator({ | ||
component_property: component._schema.get(component_property, {}) | ||
}) | ||
valid = validator.validate({component_property: value}) | ||
if not valid: | ||
error_message = dedent("""\ | ||
|
||
A Dash Callback produced an invalid value! | ||
|
||
Dash tried to update the `{component_property}` prop of the | ||
`{component_name}` with id `{component_id}` by calling the | ||
`{callback_func_name}` function with `{args}` as arguments. | ||
|
||
This function call returned `{value}`, which did not pass | ||
validation tests for the `{component_name}` component. | ||
|
||
The expected schema for the `{component_property}` prop of the | ||
`{component_name}` component is: | ||
|
||
*************************************************************** | ||
{component_schema} | ||
*************************************************************** | ||
|
||
The errors in validation are as follows: | ||
|
||
""").format( | ||
component_property=component_property, | ||
component_name=component.__name__, | ||
component_id=component_id, | ||
callback_func_name=callback_func_name, | ||
args='({})'.format(", ".join(map(repr, args))), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 😸 |
||
value=value, | ||
component_schema=pprint.pformat( | ||
component._schema[component_property] | ||
) | ||
) | ||
|
||
raise exceptions.CallbackOutputValidationError( | ||
generate_validation_error_message( | ||
validator.errors, | ||
0, | ||
error_message | ||
) | ||
) | ||
# Must also validate initialization of newly created components | ||
if component_property == 'children': | ||
if isinstance(value, Component): | ||
value.validate() | ||
for component in value.traverse(): | ||
if isinstance(component, Component): | ||
component.validate() | ||
|
||
def _validate_layout(self): | ||
if self.layout is None: | ||
|
@@ -929,6 +1018,11 @@ def _validate_layout(self): | |
|
||
component_ids = {layout_id} if layout_id else set() | ||
for component in to_validate.traverse(): | ||
if ( | ||
not self.config.suppress_validation_exceptions and | ||
isinstance(component, Component) | ||
): | ||
component.validate() | ||
component_id = getattr(component, 'id', None) | ||
if component_id and component_id in component_ids: | ||
raise exceptions.DuplicateIdError( | ||
|
@@ -1054,5 +1148,9 @@ def run_server(self, | |
:return: | ||
""" | ||
debug = self.enable_dev_tools(debug, dev_tools_serve_dev_bundles) | ||
if not debug: | ||
# Do not throw debugging exceptions in production. | ||
self.config.suppress_validation_exceptions = True | ||
self.config.suppress_callback_exceptions = True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⚡ Only suppress the props validation, we still want to validate the callbacks and it's not an expensive check performance wise. |
||
self.server.run(port=port, debug=debug, | ||
**flask_run_options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⏪