Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
49d8c2112dc611a0e2f4f7eb0dbda3ccd76377ec7bce2a2559d0fa015665fa4f88b06862057426b65a674df2bb89e5aa93b04575389d7137ffd63ba954aae393d4bf6a934c264bb97ef4cb76fe233c145cf476272817398ee3d1ab92404361373fc27e804d0bc947e6c9b693b6c37980fa74afd5bcec21dd13a800a33769c42c3d519910ad4b02ddbfcbebd954558fe603aac2f6ecaa75d7625a357ee94f067f919b5b9f2f6c15daf99490f886c83ae75d6f18d1727e87db60a6967dad42fab9806682d2906e7c903a7c08b1d1337426e4f3b0b5385d98a1a70523bebb5b7935d8eb35fb8e2c670fd11b72049dfba3d1402015674e8c2d97f9fcfa9695f20cdf92812b0a57cc564b4f5c0a515d2dce6a06708c2f7c8f2953c64bc6d8202256e3aa8d9f9db29141d0e81616583a588ce3dcead8e79962b2a3bcfefba5093a04af81bfd81fadb944bcc3b785e1bea5cf00File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -9,8 +9,11 @@ mock | ||
| tox | ||
| tox-pyenv | ||
| six | ||
| numpy | ||
| pandas | ||
| plotly>=2.0.8 | ||
| requests[security] | ||
| flake8 | ||
| pylint==2.1.1 | ||
| astroid==2.0.4 | ||
| Cerberus==1.2 | ||
| 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,11 +5,13 @@ | ||
| import collections | ||
| import importlib | ||
| import json | ||
| import pprint | ||
| import pkgutil | ||
| import warnings | ||
| import re | ||
| 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( | ||
| @@ -168,6 +177,7 @@ def _handle_error(error): | ||
| self.assets_ignore = assets_ignore | ||
| self.registered_paths = {} | ||
| self.namespaces = {} | ||
| # urls | ||
| self.routes = [] | ||
| @@ -256,7 +266,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 +584,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 +722,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 +840,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 +862,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 +877,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 +897,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 +926,88 @@ 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.
Uh oh!There was an error while loading. Please reload this page. | ||
| '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.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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))), | ||
| value=value, | ||
| component_schema=pprint.pformat( | ||
| component._schema[component_property] | ||
| ) | ||
| ) | ||
| error_message = generate_validation_error_message( | ||
| validator.errors, | ||
| 0, | ||
| error_message | ||
| ) + dedent(""" | ||
| You can turn off these validation exceptions by setting | ||
| `app.config.suppress_validation_exceptions=True` | ||
rmarren1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| """) | ||
| raise exceptions.CallbackOutputValidationError(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 +1024,11 @@ def _validate_layout(self): | ||
| component_ids = {layout_id} if layout_id else set() | ||
| for component in to_validate.traverse(): | ||
| if ( | ||
Contributor 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 ContributorAuthor 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( | ||
| @@ -1057,5 +1154,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 | ||
| self.server.run(port=port, debug=debug, | ||
| **flask_run_options) | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.