-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Validate component properties #264 #340
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 92 commits
49d8c21
12dc611
a0e2f4f
7eb0dbd
a3ccd76
377ec7b
ce2a255
9d0fa01
5665fa4
f88b068
6205742
6b65a67
4df2bb8
9e5aa93
b045753
89d7137
ffd63ba
954aae3
93d4bf6
a934c26
4bb97ef
4cb76fe
233c145
cf47627
2817398
ee3d1ab
9240436
1373fc2
7e804d0
bc947e6
c9b693b
6c37980
fa74afd
5bcec21
dd13a80
0a33769
c42c3d5
19910ad
4b02ddb
fcbebd9
54558fe
603aac2
f6ecaa7
5d7625a
357ee94
f067f91
9b5b9f2
f6c15da
f99490f
886c83a
e75d6f1
8d1727e
87db60a
6967dad
42fab98
06682d2
906e7c9
03a7c08
b1d1337
426e4f3
b0b5385
d98a1a7
0523beb
b5b7935
d8eb35f
b8e2c67
0fd11b7
2049dfb
a3d1402
015674e
8c2d97f
9fcfa96
95f20cd
f92812b
0a57cc5
64b4f5c
0a515d2
dce6a06
708c2f7
c8f2953
c64bc6d
8202256
e3aa8d9
f9db291
41d0e81
616583a
588ce3d
cead8e7
9962b2a
3bcfefb
a5093a0
4af81bf
d81fadb
944bcc3
b785e1b
ea5cf00
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==1.2 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
import collections | ||
import importlib | ||
import json | ||
import pprint | ||
import pkgutil | ||
import warnings | ||
import re | ||
|
@@ -20,6 +21,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 +87,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 +130,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( | ||
|
@@ -168,6 +176,7 @@ def _handle_error(error): | |
self.assets_ignore = assets_ignore | ||
|
||
self.registered_paths = {} | ||
self.namespaces = {} | ||
|
||
# urls | ||
self.routes = [] | ||
|
@@ -256,7 +265,6 @@ def layout(self, value): | |
'a dash component.') | ||
|
||
self._layout = value | ||
|
||
layout_value = self._layout_value() | ||
# pylint: disable=protected-access | ||
self.css._update_layout(layout_value) | ||
|
@@ -575,7 +583,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() | ||
|
||
|
@@ -713,7 +721,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, | ||
|
@@ -831,7 +839,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 | ||
|
@@ -853,13 +861,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 | ||
} | ||
} | ||
} | ||
|
@@ -870,7 +876,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 | ||
|
@@ -887,6 +896,7 @@ def add_context(*args, **kwargs): | |
mimetype='application/json' | ||
) | ||
|
||
self.callback_map[callback_id]['func'] = func | ||
self.callback_map[callback_id]['callback'] = add_context | ||
|
||
return add_context | ||
|
@@ -915,7 +925,82 @@ 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) | ||
|
||
# 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 | ||
rmarren1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
'namespace' in output and | ||
'type' in output | ||
): | ||
# 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] | ||
rmarren1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 = """ | ||
|
||
|
||
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} | ||
*************************************************************** | ||
|
||
""".replace(' ', '').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))), | ||
value=value, | ||
component_schema=pprint.pformat( | ||
component._schema[component_property] | ||
) | ||
) | ||
error_message +=\ | ||
"The errors in validation are as follows:\n\n" | ||
|
||
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: | ||
|
@@ -932,6 +1017,11 @@ def _validate_layout(self): | |
|
||
component_ids = {layout_id} if layout_id else set() | ||
for component in to_validate.traverse(): | ||
if ( | ||
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. Normally there is no 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. Not sure how else to style since the if statement is > 80 characters. PEP isn't super specific about what to do here: https://www.python.org/dev/peps/pep-0008/#multiline-if-statements. |
||
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( | ||
|
Uh oh!
There was an error while loading. Please reload this page.