From 5ac225b6a5bdbb2983e01dec4a29a15132eca609 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 09:52:00 -0500 Subject: [PATCH 01/42] Add `numpy`, `pandas`, `Cerberus` to requirements. --- .circleci/requirements/dev-requirements-py37.txt | 3 +++ .circleci/requirements/dev-requirements.txt | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.circleci/requirements/dev-requirements-py37.txt b/.circleci/requirements/dev-requirements-py37.txt index 1f59ed10b4..8c134d10a4 100644 --- a/.circleci/requirements/dev-requirements-py37.txt +++ b/.circleci/requirements/dev-requirements-py37.txt @@ -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 diff --git a/.circleci/requirements/dev-requirements.txt b/.circleci/requirements/dev-requirements.txt index dd09b07796..25372de2d8 100644 --- a/.circleci/requirements/dev-requirements.txt +++ b/.circleci/requirements/dev-requirements.txt @@ -10,7 +10,10 @@ tox tox-pyenv mock six +numpy +pandas plotly>=2.0.8 requests[security] flake8 pylint==1.9.2 +Cerberus From b1ef13107520bb972766e2cb936250b0abb48585 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 09:52:43 -0500 Subject: [PATCH 02/42] Add cerberus to setup.py install_requires --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index bee83d7eb7..c328906119 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,7 @@ 'flask-compress', 'plotly', 'dash_renderer', + 'Cerberus' ], url='https://plot.ly/dash', classifiers=[ From 9e5c370b7a3e787d3d9720f6bb45ee45f7ccc354 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 09:56:10 -0500 Subject: [PATCH 03/42] Add a `suppress_validation_exceptions` option to `dash.Dash`. --- dash/dash.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dash/dash.py b/dash/dash.py index 8c3c7d4221..cfafd5d53f 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -84,6 +84,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 +127,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( @@ -1054,5 +1059,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) From 26e425a6da749a9bc894f394c43591715f835dc9 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 09:57:45 -0500 Subject: [PATCH 04/42] Add validation exception definitions. --- dash/exceptions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dash/exceptions.py b/dash/exceptions.py index 5ec2779afb..253af107cb 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -62,5 +62,13 @@ class InvalidConfig(DashException): pass +class ComponentInitializationValidationError(DashException): + pass + + +class CallbackOutputValidationError(CallbackException): + pass + + class InvalidResourceError(DashException): pass From 10498d2775bae60d584ca3e867e8709bb859c77f Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 09:59:18 -0500 Subject: [PATCH 05/42] Add the `DashValidator` class. --- dash/development/validator.py | 107 ++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 dash/development/validator.py diff --git a/dash/development/validator.py b/dash/development/validator.py new file mode 100644 index 0000000000..ca49451bf7 --- /dev/null +++ b/dash/development/validator.py @@ -0,0 +1,107 @@ +import plotly +import cerberus + + +class DashValidator(cerberus.Validator): + types_mapping = cerberus.Validator.types_mapping.copy() + types_mapping.pop('list') # To be replaced by our custom method + types_mapping.pop('number') # To be replaced by our custom method + + def _validator_plotly_figure(self, field, value): + if not isinstance(value, (dict, plotly.graph_objs.Figure)): + self._error( + field, + "Invalid Plotly Figure: Not a dict") + if isinstance(value, dict): + try: + plotly.graph_objs.Figure(value) + except (ValueError, plotly.exceptions.PlotlyDictKeyError) as e: + self._error( + field, + "Invalid Plotly Figure:\n\n{}".format(e)) + + def _validator_options_with_unique_values(self, field, value): + if not isinstance(value, list): + self._error(field, "Invalid options: Not a dict!") + values = set() + for i, option_dict in enumerate(value): + if not isinstance(option_dict, dict): + self._error( + field, + "The option at index {} is not a dictionary!" + .format(i) + ) + if 'value' not in option_dict: + self._error( + field, + "The option at index {} does not have a 'value' key!" + .format(i) + ) + curr = option_dict['value'] + if curr in values: + self._error( + field, + ("The options list you provided was not valid. " + "More than one of the options has the value {}." + .format(curr)) + ) + values.add(curr) + + def _validate_type_list(self, value): + if isinstance(value, list): + return True + elif isinstance(value, (self.component_class, str)): + return False + try: + value_list = list(value) + if not isinstance(value_list, list): + return False + except (ValueError, TypeError): + return False + return True + + # pylint: disable=no-self-use + def _validate_type_number(self, value): + if isinstance(value, (int, float)): + return True + if isinstance(value, str): # Since int('3') works + return False + try: + int(value) + return True + except (ValueError, TypeError): + pass + try: + float(value) + return True + except (ValueError, TypeError): + pass + return False + + @classmethod + def set_component_class(cls, component_cls): + cls.component_class = component_cls + c_type = cerberus.TypeDefinition('component', (component_cls,), ()) + cls.types_mapping['component'] = c_type + d_type = cerberus.TypeDefinition('dict', (dict,), ()) + cls.types_mapping['dict'] = d_type + + +def generate_validation_error_message(errors, level=0, error_message=''): + for prop, error_tuple in errors.items(): + error_message += (' ' * level) + '* {}'.format(prop) + if len(error_tuple) == 2: + error_message += '\t<- {}\n'.format(error_tuple[0]) + error_message = generate_validation_error_message( + error_tuple[1], + level + 1, + error_message) + else: + if isinstance(error_tuple[0], str): + error_message += '\t<- {}\n'.format(error_tuple[0]) + elif isinstance(error_tuple[0], dict): + error_message = generate_validation_error_message( + error_tuple[0], + level + 1, + error_message + "\n") + return error_message From d943e2e0f414668b19151bebd213b9d9b0192664 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:04:56 -0500 Subject: [PATCH 06/42] Add a modular decode hook for loading `metadata.json` files. --- dash/development/component_loader.py | 13 +++++++++++-- tests/development/test_base_component.py | 15 +++------------ tests/development/test_component_loader.py | 8 ++++++-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 2b5e70b10f..28b203562e 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -3,7 +3,16 @@ import os from .base_component import generate_class from .base_component import generate_class_file -from .base_component import ComponentRegistry + +def _decode_hook(pairs): + new_pairs = [] + for key, value in pairs: + if type(value).__name__ == 'unicode': + value = value.encode('utf-8') + if type(key).__name__ == 'unicode': + key = key.encode('utf-8') + new_pairs.append((key, value)) + return collections.OrderedDict(new_pairs) def _get_metadata(metadata_path): @@ -11,7 +20,7 @@ def _get_metadata(metadata_path): with open(metadata_path) as data_file: json_string = data_file.read() data = json\ - .JSONDecoder(object_pairs_hook=collections.OrderedDict)\ + .JSONDecoder(object_pairs_hook=_decode_hook)\ .decode(json_string) return data diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index a43be1b898..7eef4b5540 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -7,6 +7,7 @@ import unittest import plotly +from dash.development.component_loader import _get_metadata from dash.development.base_component import ( generate_class, generate_class_string, @@ -505,12 +506,7 @@ def test_pop(self): class TestGenerateClassFile(unittest.TestCase): def setUp(self): json_path = os.path.join('tests', 'development', 'metadata_test.json') - with open(json_path) as data_file: - json_string = data_file.read() - data = json\ - .JSONDecoder(object_pairs_hook=collections.OrderedDict)\ - .decode(json_string) - self.data = data + data = _get_metadata(json_path) # Create a folder for the new component file os.makedirs('TableComponents') @@ -568,12 +564,7 @@ def test_class_file(self): class TestGenerateClass(unittest.TestCase): def setUp(self): path = os.path.join('tests', 'development', 'metadata_test.json') - with open(path) as data_file: - json_string = data_file.read() - data = json\ - .JSONDecoder(object_pairs_hook=collections.OrderedDict)\ - .decode(json_string) - self.data = data + data = _get_metadata(path) self.ComponentClass = generate_class( typename='Table', diff --git a/tests/development/test_component_loader.py b/tests/development/test_component_loader.py index a4fb4423e1..967c783f56 100644 --- a/tests/development/test_component_loader.py +++ b/tests/development/test_component_loader.py @@ -3,7 +3,11 @@ import os import shutil import unittest -from dash.development.component_loader import load_components, generate_classes +from dash.development.component_loader import ( + load_components, + generate_classes, + _decode_hook +) from dash.development.base_component import ( generate_class, Component @@ -98,7 +102,7 @@ } }''' METADATA = json\ - .JSONDecoder(object_pairs_hook=collections.OrderedDict)\ + .JSONDecoder(object_pairs_hook=_decode_hook)\ .decode(METADATA_STRING) From b12c73d9bd71add9b5ad254fc36fccef1d3f89bc Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:14:16 -0500 Subject: [PATCH 07/42] Add a `validate` method to the base component class. --- dash/development/base_component.py | 94 ++++++++++++++++++------------ 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 789253a3f2..fd6340ff5d 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -6,43 +6,10 @@ import sys import six -from ._all_keywords import kwlist +from textwrap import dedent - -# pylint: disable=no-init,too-few-public-methods -class ComponentRegistry: - """Holds a registry of the namespaces used by components.""" - - registry = set() - __dist_cache = {} - - @classmethod - def get_resources(cls, resource_name): - cached = cls.__dist_cache.get(resource_name) - - if cached: - return cached - - cls.__dist_cache[resource_name] = resources = [] - - for module_name in cls.registry: - module = sys.modules[module_name] - resources.extend(getattr(module, resource_name, [])) - - return resources - - -class ComponentMeta(abc.ABCMeta): - - # pylint: disable=arguments-differ - def __new__(mcs, name, bases, attributes): - component = abc.ABCMeta.__new__(mcs, name, bases, attributes) - module = attributes['__module__'].split('.')[0] - if name == 'Component' or module == 'builtins': - # Don't do the base component - # and the components loaded dynamically by load_component - # as it doesn't have the namespace. - return component +import dash.exceptions +from .validator import DashValidator, generate_validation_error_message ComponentRegistry.registry.add(module) @@ -280,6 +247,61 @@ def traverse_with_paths(self): for p, t in i.traverse_with_paths(): yield "\n".join([list_path, p]), t + def validate(self): + # Make sure arguments have valid values + DashValidator.set_component_class(Component) + validator = DashValidator( + self._schema, + allow_unknown=True, + ) + args = { + k: self.__dict__[k] + for k in self.__dict__['_prop_names'] + if k in self.__dict__.keys() + } + valid = validator.validate(args) + if not valid: + # pylint: disable=protected-access + error_message = dedent("""\ + + A Dash Component was initialized with invalid properties! + + Dash tried to create a `{component_name}` component with the + following arguments, which caused a validation failure: + + *************************************************************** + {component_args} + *************************************************************** + + The expected schema for the `{component_name}` component is: + + *************************************************************** + {component_schema} + *************************************************************** + + The errors in validation are as follows: + + + """).format( + component_name=self.__class__.__name__, + component_args=pprint.pformat(args), + component_schema=pprint.pformat(self.__class__._schema) + ) + + 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` + """) + + # pylint: disable=protected-access + raise dash.exceptions.ComponentInitializationValidationError( + error_message + ) + def __iter__(self): """Yield IDs in the tree of children.""" for t in self.traverse(): From c12391577dcbfcaa5310c99c628d60667b49db32 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:15:02 -0500 Subject: [PATCH 08/42] Add schema generation logic. --- dash/development/base_component.py | 135 +++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index fd6340ff5d..e03cf12234 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -333,6 +333,141 @@ def __len__(self): return length +def schema_is_nullable(type_object): + if type_object: + if type_object.get('name', None) == 'enum': + values = type_object['value'] + for v in values: + value = v['value'] + if value == 'null': + return True + if type_object.get('name', None) == 'union': + values = type_object['value'] + if any([schema_is_nullable(v) for v in values]): + return True + return False + + +def js_to_cerberus_type(type_object): + def _merge(x, y): + z = x.copy() + z.update(y) + return z + + def _enum(x): + schema = {'allowed': [], + 'type': ('string', 'number')} + values = x['value'] + for v in values: + value = v['value'] + if value == 'null': + schema.update({'nullable': True}) + schema['allowed'].append(None) + elif value == 'true': + schema['allowed'].append(True) + elif value == 'false': + schema['allowed'].append(False) + else: + string_value = v['value'].strip("'\"'") + schema['allowed'].append(string_value) + try: + int_value = int(string_value) + schema['allowed'].append(int_value) + except ValueError: + pass + try: + float_value = float(string_value) + schema['allowed'].append(float_value) + except ValueError: + pass + return schema + + converters = { + 'None': lambda x: {}, + 'func': lambda x: {}, + 'symbol': lambda x: {}, + 'custom': lambda x: {}, + 'node': lambda x: { + 'anyof': [ + {'type': 'component'}, + {'type': 'boolean'}, + {'type': 'number'}, + {'type': 'string'}, + { + 'type': 'list', + 'schema': { + 'type': ( + 'component', + 'boolean', + 'number', + 'string') + } + } + ] + }, + 'element': lambda x: {'type': 'component'}, + 'enum': _enum, + 'union': lambda x: { + 'anyof': [js_to_cerberus_type(v) for v in x['value']], + }, + 'any': lambda x: { + 'type': ('boolean', + 'number', + 'string', + 'dict', + 'list') + }, + 'string': lambda x: {'type': 'string'}, + 'bool': lambda x: {'type': 'boolean'}, + 'number': lambda x: {'type': 'number'}, + 'integer': lambda x: {'type': 'number'}, + 'object': lambda x: {'type': 'dict'}, + 'objectOf': lambda x: { + 'type': 'dict', + 'nullable': schema_is_nullable(x), + 'valueschema': js_to_cerberus_type(x['value']) + }, + 'array': lambda x: {'type': 'list'}, + 'arrayOf': lambda x: { + 'type': 'list', + 'schema': _merge( + js_to_cerberus_type(x['value']), + {'nullable': schema_is_nullable(x['value'])} + ) + }, + 'shape': lambda x: { + 'type': 'dict', + 'allow_unknown': False, + 'nullable': schema_is_nullable(x), + 'schema': { + k: js_to_cerberus_type(v) for k, v in x['value'].items() + } + }, + 'instanceOf': lambda x: dict( + Date={'type': 'datetime'}, + ).get(x['value'], {}) + } + if type_object: + converter = converters[type_object.get('name', 'None')] + schema = converter(type_object) + return schema + return {} + + +def generate_property_schema(jsonSchema): + schema = {} + type_object = jsonSchema.get('type', None) + required = jsonSchema.get('required', None) + propType = js_to_cerberus_type(type_object) + if propType: + schema.update(propType) + if schema_is_nullable(type_object): + schema.update({'nullable': True}) + if required: + schema.update({'required': True}) + return schema + + # pylint: disable=unused-argument def generate_class_string(typename, props, description, namespace): """ From 6381b54f61714a2912e6fd4e3ec4aa8ca9d15be6 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:19:11 -0500 Subject: [PATCH 09/42] Integrate schema generation with the Python class string generation. --- dash/development/base_component.py | 43 +++++++++++++++++++----------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index e03cf12234..ceab1a0756 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -84,6 +84,8 @@ def __str__(self): REQUIRED = _REQUIRED() + _schema = {} + def __init__(self, **kwargs): # pylint: disable=super-init-not-called for k, v in list(kwargs.items()): @@ -504,8 +506,13 @@ def generate_class_string(typename, props, description, namespace): # it to be `null` or whether that was just the default value. # The solution might be to deal with default values better although # not all component authors will supply those. - c = '''class {typename}(Component): + # pylint: disable=too-many-locals + c = ''' +schema = {schema} + +class {typename}(Component): """{docstring}""" + _schema = schema @_explicitize_args def __init__(self, {default_argtext}): self._prop_names = {list_of_valid_keys} @@ -521,12 +528,13 @@ def __init__(self, {default_argtext}): _explicit_args = kwargs.pop('_explicit_args') _locals = locals() _locals.update(kwargs) # For wildcard attrs - args = {{k: _locals[k] for k in _explicit_args if k != 'children'}} + args = {{k: _locals[k] for k in _explicit_args}} for k in {required_args}: if k not in args: raise TypeError( 'Required argument `' + k + '` was not specified.') + args.pop('children') super({typename}, self).__init__({argtext}) def __repr__(self): @@ -569,23 +577,26 @@ def __repr__(self): events = '[' + ', '.join(parse_events(props)) + ']' prop_keys = list(props.keys()) if 'children' in props: - prop_keys.remove('children') - default_argtext = "children=None, " - # pylint: disable=unused-variable - argtext = 'children=children, **args' + default_argtext = 'children=None, ' + argtext = 'children=children, **args' # Children will be popped before else: - default_argtext = "" + default_argtext = '' argtext = '**args' - default_argtext += ", ".join( - [('{:s}=Component.REQUIRED'.format(p) + for p in list(props.keys()): + if ( + not p.endswith("-*") and # Not a wildcard attribute + p not in keyword.kwlist and # Not a protected keyword + p not in ['dashEvents', 'fireEvent', 'setProps'] and + p != 'children' # Already accounted for + ): + default_argtext += ('{:s}=Component.REQUIRED, '.format(p) if props[p]['required'] else - '{:s}=Component.UNDEFINED'.format(p)) - for p in prop_keys - if not p.endswith("-*") and - p not in kwlist and - p not in ['dashEvents', 'fireEvent', 'setProps']] + ['**kwargs'] - ) - + '{:s}=Component.UNDEFINED, '.format(p)) + default_argtext += '**kwargs' + schema = { + k: generate_property_schema(v) + for k, v in props.items() if not k.endswith("-*") + } required_args = required_props(props) return c.format(**locals()) From 7ac65f20fe20540b09cbb3502eac99c28230f775 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:23:41 -0500 Subject: [PATCH 10/42] Re-name validation functions to be more explicit. --- dash/dash.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index cfafd5d53f..b6dcfad805 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -577,7 +577,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() @@ -715,7 +715,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, @@ -833,7 +833,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 @@ -872,7 +872,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 From 1ba24f737f0abb232d5ae8d7d2e64ff4859a9f5b Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:26:01 -0500 Subject: [PATCH 11/42] Validate callback outputs. --- dash/dash.py | 92 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index b6dcfad805..6f10272515 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -20,6 +20,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 @@ -855,13 +857,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 } } } @@ -892,6 +892,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 @@ -920,7 +921,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 + '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] + 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` + """) + + 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: From f21372bf39657502ba437d1fcafa0da9bd0dff16 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:26:26 -0500 Subject: [PATCH 12/42] Validate app initial layout. --- dash/dash.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dash/dash.py b/dash/dash.py index 6f10272515..057d1c74c8 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1019,6 +1019,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( From 398950d3fb833024ec9dc711b34aeec7204c6c5f Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:29:03 -0500 Subject: [PATCH 13/42] Update test component code (react 16, children PropType, more props) --- tests/development/TestReactComponent.react.js | 78 +++++++++------ .../TestReactComponentRequired.react.js | 20 +++- tests/development/metadata_required_test.json | 55 ++++++++++- tests/development/metadata_test.json | 99 ++++++++++++++++--- 4 files changed, 205 insertions(+), 47 deletions(-) diff --git a/tests/development/TestReactComponent.react.js b/tests/development/TestReactComponent.react.js index 5c45fed8c6..a58269e85d 100644 --- a/tests/development/TestReactComponent.react.js +++ b/tests/development/TestReactComponent.react.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; // A react component with all of the available proptypes to run tests over /** @@ -15,63 +16,66 @@ ReactComponent.propTypes = { /** * Description of optionalArray */ - optionalArray: React.PropTypes.array, - optionalBool: React.PropTypes.bool, - optionalFunc: React.PropTypes.func, - optionalNumber: React.PropTypes.number, - optionalObject: React.PropTypes.object, - optionalString: React.PropTypes.string, - optionalSymbol: React.PropTypes.symbol, + optionalArray: PropTypes.array, + optionalBool: PropTypes.bool, + optionalFunc: PropTypes.func, + optionalNumber: PropTypes.number, + optionalObject: PropTypes.object, + optionalString: PropTypes.string, + optionalSymbol: PropTypes.symbol, // Anything that can be rendered: numbers, strings, elements or an array // (or fragment) containing these types. - optionalNode: React.PropTypes.node, + optionalNode: PropTypes.node, // A React element. - optionalElement: React.PropTypes.element, + optionalElement: PropTypes.element, // You can also declare that a prop is an instance of a class. This uses // JS's instanceof operator. - optionalMessage: React.PropTypes.instanceOf(Message), + optionalMessage: PropTypes.instanceOf(Message), // You can ensure that your prop is limited to specific values by treating // it as an enum. - optionalEnum: React.PropTypes.oneOf(['News', 'Photos']), + optionalEnum: PropTypes.oneOf(['News', 'Photos', 1, 2, true, false]), - // An object that could be one of many types - optionalUnion: React.PropTypes.oneOfType([ - React.PropTypes.string, - React.PropTypes.number, - React.PropTypes.instanceOf(Message) + // An object that could be one of many types. + optionalUnion: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Message) ]), // An array of a certain type - optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), + optionalArrayOf: PropTypes.arrayOf(PropTypes.number), // An object with property values of a certain type - optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), + optionalObjectOf: PropTypes.objectOf(PropTypes.number), // An object taking on a particular shape - optionalObjectWithShapeAndNestedDescription: React.PropTypes.shape({ - color: React.PropTypes.string, - fontSize: React.PropTypes.number, + optionalObjectWithShapeAndNestedDescription: PropTypes.shape({ + color: PropTypes.string, + fontSize: PropTypes.number, /** * Figure is a plotly graph object */ - figure: React.PropTypes.shape({ + figure: PropTypes.shape({ /** * data is a collection of traces */ - data: React.PropTypes.arrayOf(React.PropTypes.object), + data: PropTypes.arrayOf(PropTypes.object), /** * layout describes the rest of the figure */ - layout: React.PropTypes.object + layout: PropTypes.object }) }), // A value of any data type - optionalAny: React.PropTypes.any, + optionalAny: PropTypes.any, + + "data-*": PropTypes.string, + "aria-*": PropTypes.string, customProp: function(props, propName, componentName) { if (!/matchme/.test(props[propName])) { @@ -82,7 +86,7 @@ ReactComponent.propTypes = { } }, - customArrayProp: React.PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) { + customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) { if (!/matchme/.test(propValue[key])) { return new Error( 'Invalid prop `' + propFullName + '` supplied to' + @@ -93,13 +97,29 @@ ReactComponent.propTypes = { // special dash events - children: React.PropTypes.node, + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool, + PropTypes.element, + PropTypes.oneOf([null]), + PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool, + PropTypes.element, + PropTypes.oneOf([null]) + ]) + ) + ]), - id: React.PropTypes.string, + in: PropTypes.string, + id: PropTypes.string, // dashEvents is a special prop that is used to events validation - dashEvents: React.PropTypes.oneOf([ + dashEvents: PropTypes.oneOf([ 'restyle', 'relayout', 'click' diff --git a/tests/development/TestReactComponentRequired.react.js b/tests/development/TestReactComponentRequired.react.js index a08b0f0dda..9b52fca332 100644 --- a/tests/development/TestReactComponentRequired.react.js +++ b/tests/development/TestReactComponentRequired.react.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; // A react component with all of the available proptypes to run tests over /** @@ -12,8 +13,23 @@ class ReactComponent extends Component { } ReactComponent.propTypes = { - children: React.PropTypes.node, - id: React.PropTypes.string.isRequired, + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool, + PropTypes.element, + PropTypes.oneOf([null]), + PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool, + PropTypes.element, + PropTypes.oneOf([null]) + ]) + ) + ]), + id: PropTypes.string.isRequired, }; export default ReactComponent; diff --git a/tests/development/metadata_required_test.json b/tests/development/metadata_required_test.json index 9b2caa62c4..3d5abb5d19 100644 --- a/tests/development/metadata_required_test.json +++ b/tests/development/metadata_required_test.json @@ -1,10 +1,63 @@ { "description": "This is a description of the component.\nIt's multiple lines long.", + "displayName": "ReactComponent", "methods": [], "props": { "children": { "type": { - "name": "node" + "name": "union", + "value": [ + { + "name": "string" + }, + { + "name": "number" + }, + { + "name": "bool" + }, + { + "name": "element" + }, + { + "name": "enum", + "value": [ + { + "value": "null", + "computed": false + } + ] + }, + { + "name": "arrayOf", + "value": { + "name": "union", + "value": [ + { + "name": "string" + }, + { + "name": "number" + }, + { + "name": "bool" + }, + { + "name": "element" + }, + { + "name": "enum", + "value": [ + { + "value": "null", + "computed": false + } + ] + } + ] + } + } + ] }, "required": false, "description": "" diff --git a/tests/development/metadata_test.json b/tests/development/metadata_test.json index 1da85ba814..97e8a3b26a 100644 --- a/tests/development/metadata_test.json +++ b/tests/development/metadata_test.json @@ -1,5 +1,6 @@ { "description": "This is a description of the component.\nIt's multiple lines long.", + "displayName": "ReactComponent", "methods": [], "props": { "optionalArray": { @@ -92,6 +93,22 @@ { "value": "'Photos'", "computed": false + }, + { + "value": "1", + "computed": false + }, + { + "value": "2", + "computed": false + }, + { + "value": "false", + "computed": false + }, + { + "value": "true", + "computed": false } ] }, @@ -181,42 +198,94 @@ "required": false, "description": "" }, - "customProp": { + "data-*": { "type": { - "name": "custom", - "raw": "function(props, propName, componentName) {\n if (!/matchme/.test(props[propName])) {\n return new Error(\n 'Invalid prop `' + propName + '` supplied to' +\n ' `' + componentName + '`. Validation failed.'\n );\n }\n}" + "name": "string" }, "required": false, "description": "" }, - "customArrayProp": { + "aria-*": { "type": { - "name": "arrayOf", - "value": { - "name": "custom", - "raw": "function(propValue, key, componentName, location, propFullName) {\n if (!/matchme/.test(propValue[key])) {\n return new Error(\n 'Invalid prop `' + propFullName + '` supplied to' +\n ' `' + componentName + '`. Validation failed.'\n );\n }\n}" - } + "name": "string" }, "required": false, "description": "" }, - "children": { + "customProp": { "type": { - "name": "node" + "name": "custom", + "raw": "function(props, propName, componentName) {\n if (!/matchme/.test(props[propName])) {\n return new Error(\n 'Invalid prop `' + propName + '` supplied to' +\n ' `' + componentName + '`. Validation failed.'\n );\n }\n}" }, "required": false, "description": "" }, - "data-*": { + "customArrayProp": { "type": { - "name": "string" + "name": "arrayOf", + "value": { + "name": "custom", + "raw": "function(propValue, key, componentName, location, propFullName) {\n if (!/matchme/.test(propValue[key])) {\n return new Error(\n 'Invalid prop `' + propFullName + '` supplied to' +\n ' `' + componentName + '`. Validation failed.'\n );\n }\n}" + } }, "required": false, "description": "" }, - "aria-*": { + "children": { "type": { - "name": "string" + "name": "union", + "value": [ + { + "name": "string" + }, + { + "name": "number" + }, + { + "name": "bool" + }, + { + "name": "element" + }, + { + "name": "enum", + "value": [ + { + "value": "null", + "computed": false + } + ] + }, + { + "name": "arrayOf", + "value": { + "name": "union", + "value": [ + { + "name": "string" + }, + { + "name": "number" + }, + { + "name": "bool" + }, + { + "name": "element" + }, + { + "name": "enum", + "value": [ + { + "value": "null", + "computed": false + } + ] + } + ] + } + } + ] }, "required": false, "description": "" From 83f70d74ec68ea163d644110b6534dc8b233f9f4 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:31:13 -0500 Subject: [PATCH 14/42] Re-build metadata_test.py --- tests/development/metadata_test.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py index 1074ff0e51..647613ec2e 100644 --- a/tests/development/metadata_test.py +++ b/tests/development/metadata_test.py @@ -3,13 +3,16 @@ from dash.development.base_component import Component, _explicitize_args + +schema = {'customArrayProp': {'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'type': 'boolean'}, 'optionalFunc': {}, 'optionalSymbol': {}, 'in': {'type': 'string'}, 'customProp': {}, 'children': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {}, 'optionalNumber': {'type': 'number'}, 'optionalObject': {'type': 'dict'}, 'dashEvents': {'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'type': 'string'}, 'optionalString': {'type': 'string'}, 'optionalElement': {'type': 'component'}, 'optionalArray': {'type': 'list'}, 'optionalNode': {'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}]}, 'optionalObjectOf': {'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'anyof': [{'type': 'string'}, {'type': 'number'}, {}]}, 'optionalAny': {'type': ('boolean', 'number', 'string', 'dict', 'list')}} + class Table(Component): """A Table component. This is a description of the component. It's multiple lines long. Keyword arguments: -- children (a list of or a singular dash component, string or number; optional) +- children (string | number | boolean | dash component | a value equal to: null | list; optional) - optionalArray (list; optional): Description of optionalArray - optionalBool (boolean; optional) - optionalNumber (number; optional) @@ -17,7 +20,7 @@ class Table(Component): - optionalString (string; optional) - optionalNode (a list of or a singular dash component, string or number; optional) - optionalElement (dash component; optional) -- optionalEnum (a value equal to: 'News', 'Photos'; optional) +- optionalEnum (a value equal to: 'News', 'Photos', 1, 2, false, true; optional) - optionalUnion (string | number; optional) - optionalArrayOf (list; optional) - optionalObjectOf (dict with strings as keys and values of type number; optional) @@ -30,33 +33,35 @@ class Table(Component): - data (list; optional): data is a collection of traces - layout (dict; optional): layout describes the rest of the figure - optionalAny (boolean | number | string | dict | list; optional) -- customProp (optional) -- customArrayProp (list; optional) - data-* (string; optional) - aria-* (string; optional) +- customProp (optional) +- customArrayProp (list; optional) - in (string; optional) - id (string; optional) Available events: 'restyle', 'relayout', 'click'""" + _schema = schema @_explicitize_args def __init__(self, children=None, optionalArray=Component.UNDEFINED, optionalBool=Component.UNDEFINED, optionalFunc=Component.UNDEFINED, optionalNumber=Component.UNDEFINED, optionalObject=Component.UNDEFINED, optionalString=Component.UNDEFINED, optionalSymbol=Component.UNDEFINED, optionalNode=Component.UNDEFINED, optionalElement=Component.UNDEFINED, optionalMessage=Component.UNDEFINED, optionalEnum=Component.UNDEFINED, optionalUnion=Component.UNDEFINED, optionalArrayOf=Component.UNDEFINED, optionalObjectOf=Component.UNDEFINED, optionalObjectWithShapeAndNestedDescription=Component.UNDEFINED, optionalAny=Component.UNDEFINED, customProp=Component.UNDEFINED, customArrayProp=Component.UNDEFINED, id=Component.UNDEFINED, **kwargs): - self._prop_names = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', 'in', 'id'] + self._prop_names = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'data-*', 'aria-*', 'customProp', 'customArrayProp', 'in', 'id'] self._type = 'Table' self._namespace = 'TableComponents' self._valid_wildcard_attributes = ['data-', 'aria-'] self.available_events = ['restyle', 'relayout', 'click'] - self.available_properties = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', 'in', 'id'] + self.available_properties = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'data-*', 'aria-*', 'customProp', 'customArrayProp', 'in', 'id'] self.available_wildcard_properties = ['data-', 'aria-'] _explicit_args = kwargs.pop('_explicit_args') _locals = locals() _locals.update(kwargs) # For wildcard attrs - args = {k: _locals[k] for k in _explicit_args if k != 'children'} + args = {k: _locals[k] for k in _explicit_args} for k in []: if k not in args: raise TypeError( 'Required argument `' + k + '` was not specified.') + args.pop('children', None) super(Table, self).__init__(children=children, **args) def __repr__(self): From 088329062f3ae495c9aee40f70145f1e47c1588d Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:32:43 -0500 Subject: [PATCH 15/42] Update base component tests that would have broken given component changes. --- tests/development/test_base_component.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 7eef4b5540..529ce116a9 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -610,14 +610,14 @@ def test_to_plotly_json(self): } }) - c = self.ComponentClass(id='my-id', optionalArray=None) + c = self.ComponentClass(id='my-id', optionalArray=[]) self.assertEqual(c.to_plotly_json(), { 'namespace': 'TableComponents', 'type': 'Table', 'props': { 'children': None, 'id': 'my-id', - 'optionalArray': None + 'optionalArray': [] } }) @@ -754,7 +754,7 @@ def setUp(self): self.expected_arg_strings = OrderedDict([ ['children', - 'a list of or a singular dash component, string or number'], + 'string | number | boolean | dash component | a value equal to: null | list'], ['optionalArray', 'list'], @@ -777,7 +777,7 @@ def setUp(self): ['optionalMessage', ''], - ['optionalEnum', 'a value equal to: \'News\', \'Photos\''], + ['optionalEnum', 'a value equal to: \'News\', \'Photos\', 1, 2, false, true'], ['optionalUnion', 'string | number'], @@ -844,7 +844,7 @@ def assert_docstring(assertEqual, docstring): "It's multiple lines long.", '', "Keyword arguments:", - "- children (a list of or a singular dash component, string or number; optional)", # noqa: E501 + "- children (string | number | boolean | dash component | a value equal to: null | list; optional)", # noqa: E501 "- optionalArray (list; optional): Description of optionalArray", "- optionalBool (boolean; optional)", "- optionalNumber (number; optional)", @@ -855,7 +855,7 @@ def assert_docstring(assertEqual, docstring): "string or number; optional)", "- optionalElement (dash component; optional)", - "- optionalEnum (a value equal to: 'News', 'Photos'; optional)", + "- optionalEnum (a value equal to: 'News', 'Photos', 1, 2, false, true; optional)", "- optionalUnion (string | number; optional)", "- optionalArrayOf (list; optional)", @@ -884,10 +884,10 @@ def assert_docstring(assertEqual, docstring): "- optionalAny (boolean | number | string | dict | " "list; optional)", - "- customProp (optional)", - "- customArrayProp (list; optional)", '- data-* (string; optional)', '- aria-* (string; optional)', + "- customProp (optional)", + "- customArrayProp (list; optional)", '- in (string; optional)', '- id (string; optional)', '', From 422c2e2fd4021b74bdbea4c988fcd073384192b1 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:33:20 -0500 Subject: [PATCH 16/42] Add test for the generated schema. --- tests/development/test_base_component.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 529ce116a9..15a02c5a13 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -545,6 +545,14 @@ def setUp(self): with open(expected_string_path, 'r') as f: self.expected_class_string = f.read() + def remove_schema(string): + tmp = string.split("\n") + return "\n".join(tmp[:6] + tmp[7:]) + self.expected_class_string = remove_schema(self.expected_class_string) + self.component_class_string =\ + remove_schema(self.component_class_string) + self.written_class_string = remove_schema(self.written_class_string) + def tearDown(self): shutil.rmtree('TableComponents') @@ -732,6 +740,12 @@ def test_call_signature(self): ['None'] + ['undefined'] * 19 ) + def test_schema_generation(self): + self.assertEqual( + self.ComponentClass._schema, + {'customArrayProp': {'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'type': 'boolean'}, 'optionalFunc': {}, 'optionalSymbol': {}, 'in': {'type': 'string'}, 'customProp': {}, 'children': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {}, 'optionalNumber': {'type': 'number'}, 'optionalObject': {'type': 'dict'}, 'dashEvents': {'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'type': 'string'}, 'optionalString': {'type': 'string'}, 'optionalElement': {'type': 'component'}, 'optionalArray': {'type': 'list'}, 'optionalNode': {'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}]}, 'optionalObjectOf': {'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'anyof': [{'type': 'string'}, {'type': 'number'}, {}]}, 'optionalAny': {'type': ('boolean', 'number', 'string', 'dict', 'list')}} + ) + def test_required_props(self): with self.assertRaises(Exception): self.ComponentClassRequired() From 3ff6dd495865486c6c80ee825c442c2d7f8b3e12 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:34:54 -0500 Subject: [PATCH 17/42] Update component loader tests. --- tests/development/test_component_loader.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/development/test_component_loader.py b/tests/development/test_component_loader.py index 967c783f56..0b92f01d78 100644 --- a/tests/development/test_component_loader.py +++ b/tests/development/test_component_loader.py @@ -1,8 +1,7 @@ -import collections -import json import os import shutil import unittest +import json from dash.development.component_loader import ( load_components, generate_classes, @@ -31,7 +30,7 @@ }, "children": { "type": { - "name": "object" + "name": "node" }, "description": "Children", "required": false @@ -93,7 +92,7 @@ }, "children": { "type": { - "name": "object" + "name": "node" }, "description": "Children", "required": false @@ -132,7 +131,7 @@ def test_loadcomponents(self): c = load_components(METADATA_PATH) MyComponentKwargs = { - 'foo': 'Hello World', + 'foo': 42, 'bar': 'Lah Lah', 'baz': 'Lemons', 'data-foo': 'Blah', @@ -199,6 +198,7 @@ def test_loadcomponents(self): 'baz': 'Lemons', 'data-foo': 'Blah', 'aria-bar': 'Seven', + 'baz': 'Lemons', 'children': 'Child' } AKwargs = { From b81084a447edf44ba0d40e0bfa540cea53c8a43b Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:35:09 -0500 Subject: [PATCH 18/42] Add component validation tests. --- .../development/test_component_validation.py | 511 ++++++++++++++++++ 1 file changed, 511 insertions(+) create mode 100644 tests/development/test_component_validation.py diff --git a/tests/development/test_component_validation.py b/tests/development/test_component_validation.py new file mode 100644 index 0000000000..d1f2ce9a79 --- /dev/null +++ b/tests/development/test_component_validation.py @@ -0,0 +1,511 @@ +import os +import json +import unittest +import collections +import numpy as np +import pandas as pd +import plotly.graph_objs as go +import dash +import dash_html_components as html +from importlib import import_module +from dash.development.component_loader import _get_metadata +from dash.development.base_component import generate_class, Component +from dash.development.validator import DashValidator + +# Monkey patched html +html.Div._schema = {'children': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'allowed': [None], 'type': ('string', 'number'), 'nullable': True}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'allowed': [None], 'type': ('string', 'number'), 'nullable': True}], 'nullable': True}}], 'nullable': True}} +html.Button._schema = html.Div._schema + +class TestComponentValidation(unittest.TestCase): + def setUp(self): + self.validator = DashValidator + path = os.path.join('tests', 'development', 'metadata_test.json') + data = _get_metadata(path) + + self.ComponentClass = generate_class( + typename='Table', + props=data['props'], + description=data['description'], + namespace='TableComponents' + ) + + path = os.path.join( + 'tests', 'development', 'metadata_required_test.json' + ) + with open(path) as data_file: + json_string = data_file.read() + required_data = json\ + .JSONDecoder(object_pairs_hook=collections.OrderedDict)\ + .decode(json_string) + self.required_data = required_data + + self.ComponentClassRequired = generate_class( + typename='TableRequired', + props=required_data['props'], + description=required_data['description'], + namespace='TableComponents' + ) + + DashValidator.set_component_class(Component) + + def make_validator(schema): + return DashValidator(schema, allow_unknown=True) + + self.component_validator = make_validator(self.ComponentClass._schema) + self.required_validator =\ + make_validator(self.ComponentClassRequired._schema) + self.figure_validator = make_validator({ + 'figure': { + 'validator': 'plotly_figure' + } + }) + self.options_validator = make_validator({ + 'options': { + 'validator': 'options_with_unique_values' + } + }) + + def test_component_in_initial_layout_is_validated(self): + app = dash.Dash(__name__) + app.config['suppress_callback_exceptions'] = True + + app.layout = html.Div(self.ComponentClass(id='hello', children=[[]])) + + with self.assertRaises( + dash.exceptions.ComponentInitializationValidationError + ) as cm: + app._validate_layout() + the_exception = cm.exception + print(the_exception) + + def test_callback_output_is_validated(self): + app = dash.Dash(__name__) + app.config['suppress_callback_exceptions'] = True + + app.layout = html.Div(children=[ + html.Button(id='put-components', children='Click me'), + html.Div(id='container'), + ]) + + @app.callback( + dash.dependencies.Output('container', 'children'), + [dash.dependencies.Input('put-components', 'n_clicks')] + ) + def put_components(n_clicks): + if n_clicks: + return [[]] + return "empty" + + with app.server.test_request_context( + "/_dash-update-component", + json={ + 'inputs': [{ + 'id': 'put-components', + 'property': 'n_clicks', + 'value': 1 + }], + 'output': { + 'namespace': 'dash_html_components', + 'type': 'Div', + 'id': 'container', + 'property': 'children' + } + } + ): + with self.assertRaises( + dash.exceptions.CallbackOutputValidationError + ) as cm: + app.dispatch() + the_exception = cm.exception + print(the_exception) + + def test_component_initialization_in_callback_is_validated(self): + app = dash.Dash(__name__) + app.config['suppress_callback_exceptions'] = True + + app.layout = html.Div(children=[ + html.Button(id='put-components', children='Click me'), + html.Div(id='container'), + ]) + + @app.callback( + dash.dependencies.Output('container', 'children'), + [dash.dependencies.Input('put-components', 'n_clicks')] + ) + def put_components(n_clicks): + if n_clicks: + return html.Button( + children=[[]], + ) + return "empty" + + with app.server.test_request_context( + "/_dash-update-component", + json={ + 'inputs': [{ + 'id': 'put-components', + 'property': 'n_clicks', + 'value': 1 + }], + 'output': { + 'namespace': 'dash_html_components', + 'type': 'Div', + 'id': 'container', + 'property': 'children' + } + } + ): + self.assertRaises( + dash.exceptions.ComponentInitializationValidationError, + app.dispatch + ) + + def test_required_validation(self): + self.assertTrue(self.required_validator.validate({ + 'id': 'required', + 'children': 'hello world' + })) + self.assertFalse(self.required_validator.validate({ + 'children': 'hello world' + })) + + def test_string_validation(self): + self.assertTrue(self.component_validator.validate({ + 'optionalString': "bananas" + })) + self.assertFalse(self.component_validator.validate({ + 'optionalString': 7 + })) + self.assertFalse(self.component_validator.validate({ + 'optionalString': None + })) + + def test_boolean_validation(self): + self.assertTrue(self.component_validator.validate({ + 'optionalBool': False + })) + self.assertFalse(self.component_validator.validate({ + 'optionalBool': "False" + })) + self.assertFalse(self.component_validator.validate({ + 'optionalBool': None + })) + + def test_number_validation(self): + numpy_types = [ + np.int_, np.intc, np.intp, np.int8, np.int16, np.int32, np.int64, + np.uint8, np.uint16, np.uint32, np.uint64, + np.float_, np.float32, np.float64 + ] + for t in numpy_types: + self.assertTrue(self.component_validator.validate({ + 'optionalNumber': t(7) + })) + self.assertTrue(self.component_validator.validate({ + 'optionalNumber': 7 + })) + self.assertFalse(self.component_validator.validate({ + 'optionalNumber': "seven" + })) + self.assertFalse(self.component_validator.validate({ + 'optionalNumber': None + })) + + def test_object_validation(self): + self.assertTrue(self.component_validator.validate({ + 'optionalObject': {'foo': 'bar'} + })) + self.assertFalse(self.component_validator.validate({ + 'optionalObject': "not a dict" + })) + self.assertFalse(self.component_validator.validate({ + 'optionalObject': self.ComponentClass() + })) + self.assertFalse(self.component_validator.validate({ + 'optionalObject': None + })) + + def test_children_validation(self): + self.assertTrue(self.component_validator.validate({})) + self.assertTrue(self.component_validator.validate({ + 'children': None + })) + self.assertTrue(self.component_validator.validate({ + 'children': 'one' + })) + self.assertTrue(self.component_validator.validate({ + 'children': 1 + })) + self.assertTrue(self.component_validator.validate({ + 'children': False + })) + self.assertTrue(self.component_validator.validate({ + 'children': self.ComponentClass() + })) + self.assertTrue(self.component_validator.validate({ + 'children': ['one'] + })) + self.assertTrue(self.component_validator.validate({ + 'children': [1] + })) + self.assertTrue(self.component_validator.validate({ + 'children': [self.ComponentClass()] + })) + self.assertTrue(self.component_validator.validate({ + 'children': [None] + })) + self.assertTrue(self.component_validator.validate({ + 'children': () + })) + self.assertFalse(self.component_validator.validate({ + 'children': [[]] + })) + + def test_node_validation(self): + self.assertTrue(self.component_validator.validate({ + 'optionalNode': 7 + })) + self.assertTrue(self.component_validator.validate({ + 'optionalNode': "seven" + })) + self.assertFalse(self.component_validator.validate({ + 'optionalNode': None + })) + self.assertTrue(self.component_validator.validate({ + 'optionalNode': False + })) + self.assertTrue(self.component_validator.validate({ + 'optionalNode': self.ComponentClass() + })) + self.assertTrue(self.component_validator.validate({ + 'optionalNode': [ + 7, + 'seven', + False, + self.ComponentClass() + ] + })) + self.assertFalse(self.component_validator.validate({ + 'optionalNode': [["Invalid Nested Dict"]] + })) + + def test_element_validation(self): + self.assertTrue(self.component_validator.validate({ + 'optionalElement': self.ComponentClass() + })) + self.assertFalse(self.component_validator.validate({ + 'optionalElement': 7 + })) + self.assertFalse(self.component_validator.validate({ + 'optionalElement': "seven" + })) + self.assertFalse(self.component_validator.validate({ + 'optionalElement': False + })) + self.assertFalse(self.component_validator.validate({ + 'optionalElement': None + })) + + def test_enum_validation(self): + self.assertTrue(self.component_validator.validate({ + 'optionalEnum': "News" + })) + self.assertTrue(self.component_validator.validate({ + 'optionalEnum': "Photos" + })) + self.assertTrue(self.component_validator.validate({ + 'optionalEnum': 1 + })) + self.assertTrue(self.component_validator.validate({ + 'optionalEnum': 1.0 + })) + self.assertTrue(self.component_validator.validate({ + 'optionalEnum': "1" + })) + self.assertTrue(self.component_validator.validate({ + 'optionalEnum': True + })) + self.assertTrue(self.component_validator.validate({ + 'optionalEnum': False + })) + self.assertFalse(self.component_validator.validate({ + 'optionalEnum': "not_in_enum" + })) + self.assertFalse(self.component_validator.validate({ + 'optionalEnum': None + })) + + def test_union_validation(self): + self.assertTrue(self.component_validator.validate({ + 'optionalUnion': "string" + })) + self.assertTrue(self.component_validator.validate({ + 'optionalUnion': 7 + })) + # These will pass since propTypes.instanceOf(Message) + # is used in the union. We cannot validate this value, so + # we must accept everything since anything could be valid. + # TODO: Find some sort of workaround + + # self.assertFalse(self.component_validator.validate({ + # 'optionalUnion': self.ComponentClass() + # })) + # self.assertFalse(self.component_validator.validate({ + # 'optionalUnion': [1, 2, 3] + # })) + self.assertFalse(self.component_validator.validate({ + 'optionalUnion': None + })) + + def test_arrayof_validation(self): + self.assertTrue(self.component_validator.validate({ + 'optionalArrayOf': [1, 2, 3] + })) + self.assertTrue(self.component_validator.validate({ + 'optionalArrayOf': np.array([1, 2, 3]) + })) + self.assertTrue(self.component_validator.validate({ + 'optionalArrayOf': pd.Series([1, 2, 3]) + })) + self.assertFalse(self.component_validator.validate({ + 'optionalArrayOf': 7 + })) + self.assertFalse(self.component_validator.validate({ + 'optionalArrayOf': ["one", "two", "three"] + })) + self.assertFalse(self.component_validator.validate({ + 'optionalArrayOf': None + })) + + def test_objectof_validation(self): + self.assertTrue(self.component_validator.validate({ + 'optionalObjectOf': {'one': 1, 'two': 2, 'three': 3} + })) + self.assertFalse(self.component_validator.validate({ + 'optionalObjectOf': {'one': 1, 'two': '2', 'three': 3} + })) + self.assertFalse(self.component_validator.validate({ + 'optionalObjectOf': [1, 2, 3] + })) + self.assertFalse(self.component_validator.validate({ + 'optionalObjectOf': None + })) + + def test_object_with_shape_and_nested_description_validation(self): + self.assertTrue(self.component_validator.validate({ + 'optionalObjectWithShapeAndNestedDescription': { + 'color': "#431234", + 'fontSize': 2, + 'figure': { + 'data': [{'object_1': "hey"}, {'object_2': "ho"}], + 'layout': {"my": "layout"} + }, + } + })) + self.assertFalse(self.component_validator.validate({ + 'optionalObjectWithShapeAndNestedDescription': { + 'color': False, + 'fontSize': 2, + 'figure': { + 'data': [{'object_1': "hey"}, {'object_2': "ho"}], + 'layout': {"my": "layout"} + }, + } + })) + self.assertFalse(self.component_validator.validate({ + 'optionalObjectWithShapeAndNestedDescription': { + 'color': "#431234", + 'fontSize': "BAD!", + 'figure': { + 'data': [{'object_1': "hey"}, {'object_2': "ho"}], + 'layout': {"my": "layout"} + }, + } + })) + self.assertFalse(self.component_validator.validate({ + 'optionalObjectWithShapeAndNestedDescription': { + 'color': "#431234", + 'fontSize': 2, + 'figure': { + 'data': [{'object_1': "hey"}, 7], + 'layout': {"my": "layout"} + }, + } + })) + self.assertFalse(self.component_validator.validate({ + 'optionalObjectWithShapeAndNestedDescription': { + 'color': "#431234", + 'fontSize': 2, + 'figure': { + 'data': [{'object_1': "hey"}, {'object_2': "ho"}], + 'layout': ["my", "layout"] + }, + } + })) + self.assertFalse(self.component_validator.validate({ + 'optionalObjectWithShapeAndNestedDescription': None + })) + + def test_any_validation(self): + self.assertTrue(self.component_validator.validate({ + 'optionalAny': 7 + })) + self.assertTrue(self.component_validator.validate({ + 'optionalAny': "seven" + })) + self.assertTrue(self.component_validator.validate({ + 'optionalAny': False + })) + self.assertTrue(self.component_validator.validate({ + 'optionalAny': [] + })) + self.assertTrue(self.component_validator.validate({ + 'optionalAny': {} + })) + self.assertFalse(self.component_validator.validate({ + 'optionalAny': self.ComponentClass() + })) + self.assertFalse(self.component_validator.validate({ + 'optionalAny': None + })) + + def test_figure_validation(self): + self.assertFalse(self.figure_validator.validate({ + 'figure': 7 + })) + self.assertFalse(self.figure_validator.validate({ + 'figure': {} + })) + self.assertTrue(self.figure_validator.validate({ + 'figure': {'data': [{'x': [1, 2, 3], + 'y': [1, 2, 3], + 'type': 'scatter'}]} + })) + self.assertTrue(self.figure_validator.validate({ + 'figure': go.Figure( + data=[go.Scatter(x=[1, 2, 3], y=[1, 2, 3])], + layout=go.Layout() + ) + })) + self.assertFalse(self.figure_validator.validate({ + 'figure': {'doto': [{'x': [1, 2, 3], + 'y': [1, 2, 3], + 'type': 'scatter'}]} + })) + self.assertFalse(self.figure_validator.validate({ + 'figure': None + })) + + def test_options_validation(self): + self.assertFalse(self.options_validator.validate({ + 'options': [ + {'value': 'value1', 'label': 'label1'}, + {'value': 'value1', 'label': 'label1'} + ] + })) + self.assertTrue(self.options_validator.validate({ + 'options': [ + {'value': 'value1', 'label': 'label1'}, + {'value': 'value2', 'label': 'label2'} + ] + })) From 27d7da594482942d7d74e7313706e5c632a52155 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 10:35:17 -0500 Subject: [PATCH 19/42] Run component validation tests in CI. --- .circleci/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 60d68cbdae..a18b9361fb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -40,9 +40,7 @@ jobs: command: | . venv/bin/activate pylint dash setup.py --rcfile=$PYLINTRC - pylint tests -d all -e C0410,C0411,C0412,C0413,W0109 flake8 dash setup.py - flake8 --ignore=E123,E126,E501,E722,E731,F401,F841,W503,W504 --exclude=metadata_test.py tests - run: name: Run tests @@ -51,6 +49,7 @@ jobs: python --version python -m unittest tests.development.test_base_component python -m unittest tests.development.test_component_loader + python -m unittest tests.development.test_component_validation python -m unittest tests.test_integration python -m unittest tests.test_resources python -m unittest tests.test_configs From 7aa7060590b2b472bf9c5b956d9a02c1376720a2 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 11:18:04 -0500 Subject: [PATCH 20/42] Add some lines forgotten during rebase. --- dash/dash.py | 2 ++ dash/development/base_component.py | 42 ++++++++++++++++++++++++++-- dash/development/component_loader.py | 1 + 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 057d1c74c8..920f0a7932 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -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 diff --git a/dash/development/base_component.py b/dash/development/base_component.py index ceab1a0756..ad09d0878c 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -5,11 +5,49 @@ import abc import sys import six +import pprint from textwrap import dedent import dash.exceptions from .validator import DashValidator, generate_validation_error_message +from ._all_keywords import kwlist + + +# pylint: disable=no-init,too-few-public-methods +class ComponentRegistry: + """Holds a registry of the namespaces used by components.""" + + registry = set() + __dist_cache = {} + + @classmethod + def get_resources(cls, resource_name): + cached = cls.__dist_cache.get(resource_name) + + if cached: + return cached + + cls.__dist_cache[resource_name] = resources = [] + + for module_name in cls.registry: + module = sys.modules[module_name] + resources.extend(getattr(module, resource_name, [])) + + return resources + + +class ComponentMeta(abc.ABCMeta): + + # pylint: disable=arguments-differ + def __new__(mcs, name, bases, attributes): + component = abc.ABCMeta.__new__(mcs, name, bases, attributes) + module = attributes['__module__'].split('.')[0] + if name == 'Component' or module == 'builtins': + # Don't do the base component + # and the components loaded dynamically by load_component + # as it doesn't have the namespace. + return component ComponentRegistry.registry.add(module) @@ -534,7 +572,7 @@ def __init__(self, {default_argtext}): if k not in args: raise TypeError( 'Required argument `' + k + '` was not specified.') - args.pop('children') + args.pop('children', None) super({typename}, self).__init__({argtext}) def __repr__(self): @@ -585,7 +623,7 @@ def __repr__(self): for p in list(props.keys()): if ( not p.endswith("-*") and # Not a wildcard attribute - p not in keyword.kwlist and # Not a protected keyword + p not in kwlist and # Not a protected keyword p not in ['dashEvents', 'fireEvent', 'setProps'] and p != 'children' # Already accounted for ): diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 28b203562e..300d47611e 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -3,6 +3,7 @@ import os from .base_component import generate_class from .base_component import generate_class_file +from .base_component import ComponentRegistry def _decode_hook(pairs): new_pairs = [] From 28b8a13bdc625a4754cfddf147567fcae99493b2 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 11:21:45 -0500 Subject: [PATCH 21/42] Pylint fixes. --- dash/development/base_component.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index ad09d0878c..7feb217883 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -4,11 +4,11 @@ import inspect import abc import sys -import six import pprint - from textwrap import dedent +import six + import dash.exceptions from .validator import DashValidator, generate_validation_error_message from ._all_keywords import kwlist @@ -628,7 +628,7 @@ def __repr__(self): p != 'children' # Already accounted for ): default_argtext += ('{:s}=Component.REQUIRED, '.format(p) - if props[p]['required'] else + if props[p]['required'] else '{:s}=Component.UNDEFINED, '.format(p)) default_argtext += '**kwargs' schema = { From ee12470d7836d6b73d6a42ab181dcb646a743329 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 12:01:14 -0500 Subject: [PATCH 22/42] Flake8 fixes. --- dash/development/component_loader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 300d47611e..8d7a10cfed 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -5,6 +5,7 @@ from .base_component import generate_class_file from .base_component import ComponentRegistry + def _decode_hook(pairs): new_pairs = [] for key, value in pairs: From e817d752d935aa19a2c24859e42ceebb69c78966 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 14:36:59 -0500 Subject: [PATCH 23/42] Make style changes in response to code review. --- dash/dash.py | 19 ++++++-------- dash/development/base_component.py | 32 +++++++++--------------- dash/development/validator.py | 6 ++++- tests/development/metadata_test.py | 2 +- tests/development/test_base_component.py | 2 +- 5 files changed, 27 insertions(+), 34 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 920f0a7932..0013ebbd73 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -928,7 +928,7 @@ def dispatch(self): # 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 + not self.config.suppress_validation_exceptions and 'namespace' in output and 'type' in output ): @@ -988,16 +988,13 @@ def _validate_callback_output(self, namespace, component_type, ) ) - 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` - """) - - raise exceptions.CallbackOutputValidationError(error_message) + 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): diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 7feb217883..34400a0ec3 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -328,18 +328,12 @@ def validate(self): component_schema=pprint.pformat(self.__class__._schema) ) - 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` - """) - - # pylint: disable=protected-access raise dash.exceptions.ComponentInitializationValidationError( - error_message + generate_validation_error_message( + validator.errors, + 0, + error_message + ) ) def __iter__(self): @@ -374,16 +368,16 @@ def __len__(self): def schema_is_nullable(type_object): - if type_object: - if type_object.get('name', None) == 'enum': + if type_object and 'name' in type_object: + if type_object['name'] == 'enum': values = type_object['value'] for v in values: value = v['value'] if value == 'null': return True - if type_object.get('name', None) == 'union': + elif type_object['name'] == 'union': values = type_object['value'] - if any([schema_is_nullable(v) for v in values]): + if any(schema_is_nullable(v) for v in values): return True return False @@ -401,7 +395,7 @@ def _enum(x): for v in values: value = v['value'] if value == 'null': - schema.update({'nullable': True}) + schema['nullable'] = True schema['allowed'].append(None) elif value == 'true': schema['allowed'].append(True) @@ -501,10 +495,8 @@ def generate_property_schema(jsonSchema): propType = js_to_cerberus_type(type_object) if propType: schema.update(propType) - if schema_is_nullable(type_object): - schema.update({'nullable': True}) - if required: - schema.update({'required': True}) + schema['nullable'] = schema_is_nullable(type_object) + schema['required'] = required return schema diff --git a/dash/development/validator.py b/dash/development/validator.py index ca49451bf7..6b7cfd3fd0 100644 --- a/dash/development/validator.py +++ b/dash/development/validator.py @@ -1,6 +1,6 @@ import plotly import cerberus - +from textwrap import dedent class DashValidator(cerberus.Validator): types_mapping = cerberus.Validator.types_mapping.copy() @@ -104,4 +104,8 @@ def generate_validation_error_message(errors, level=0, error_message=''): error_tuple[0], level + 1, error_message + "\n") + error_message += dedent(""" + You can turn off these validation exceptions by setting + `app.config.suppress_validation_exceptions=True` + """) return error_message diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py index 647613ec2e..4ebd819f03 100644 --- a/tests/development/metadata_test.py +++ b/tests/development/metadata_test.py @@ -4,7 +4,7 @@ -schema = {'customArrayProp': {'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'type': 'boolean'}, 'optionalFunc': {}, 'optionalSymbol': {}, 'in': {'type': 'string'}, 'customProp': {}, 'children': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {}, 'optionalNumber': {'type': 'number'}, 'optionalObject': {'type': 'dict'}, 'dashEvents': {'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'type': 'string'}, 'optionalString': {'type': 'string'}, 'optionalElement': {'type': 'component'}, 'optionalArray': {'type': 'list'}, 'optionalNode': {'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}]}, 'optionalObjectOf': {'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'anyof': [{'type': 'string'}, {'type': 'number'}, {}]}, 'optionalAny': {'type': ('boolean', 'number', 'string', 'dict', 'list')}} +schema = {'customArrayProp': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'required': False, 'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'required': False, 'type': 'boolean', 'nullable': False}, 'optionalFunc': {'required': False, 'nullable': False}, 'optionalSymbol': {'required': False, 'nullable': False}, 'in': {'required': False, 'type': 'string', 'nullable': False}, 'customProp': {'required': False, 'nullable': False}, 'children': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {'required': False, 'nullable': False}, 'optionalNumber': {'required': False, 'type': 'number', 'nullable': False}, 'optionalObject': {'required': False, 'type': 'dict', 'nullable': False}, 'dashEvents': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'required': False, 'type': 'string', 'nullable': False}, 'optionalString': {'required': False, 'type': 'string', 'nullable': False}, 'optionalElement': {'required': False, 'type': 'component', 'nullable': False}, 'optionalArray': {'required': False, 'type': 'list', 'nullable': False}, 'optionalNode': {'required': False, 'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}], 'nullable': False}, 'optionalObjectOf': {'required': False, 'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {}], 'nullable': False}, 'optionalAny': {'required': False, 'type': ('boolean', 'number', 'string', 'dict', 'list'), 'nullable': False}} class Table(Component): """A Table component. diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 15a02c5a13..507ab12160 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -743,7 +743,7 @@ def test_call_signature(self): def test_schema_generation(self): self.assertEqual( self.ComponentClass._schema, - {'customArrayProp': {'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'type': 'boolean'}, 'optionalFunc': {}, 'optionalSymbol': {}, 'in': {'type': 'string'}, 'customProp': {}, 'children': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {}, 'optionalNumber': {'type': 'number'}, 'optionalObject': {'type': 'dict'}, 'dashEvents': {'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'type': 'string'}, 'optionalString': {'type': 'string'}, 'optionalElement': {'type': 'component'}, 'optionalArray': {'type': 'list'}, 'optionalNode': {'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}]}, 'optionalObjectOf': {'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'anyof': [{'type': 'string'}, {'type': 'number'}, {}]}, 'optionalAny': {'type': ('boolean', 'number', 'string', 'dict', 'list')}} + {'customArrayProp': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'required': False, 'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'required': False, 'type': 'boolean', 'nullable': False}, 'optionalFunc': {'required': False, 'nullable': False}, 'optionalSymbol': {'required': False, 'nullable': False}, 'in': {'required': False, 'type': 'string', 'nullable': False}, 'customProp': {'required': False, 'nullable': False}, 'children': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {'required': False, 'nullable': False}, 'optionalNumber': {'required': False, 'type': 'number', 'nullable': False}, 'optionalObject': {'required': False, 'type': 'dict', 'nullable': False}, 'dashEvents': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'required': False, 'type': 'string', 'nullable': False}, 'optionalString': {'required': False, 'type': 'string', 'nullable': False}, 'optionalElement': {'required': False, 'type': 'component', 'nullable': False}, 'optionalArray': {'required': False, 'type': 'list', 'nullable': False}, 'optionalNode': {'required': False, 'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}], 'nullable': False}, 'optionalObjectOf': {'required': False, 'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {}], 'nullable': False}, 'optionalAny': {'required': False, 'type': ('boolean', 'number', 'string', 'dict', 'list'), 'nullable': False}} ) def test_required_props(self): From 6b8cfd305a751b7164a013f52c1eebeaa8245163 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 8 Nov 2018 14:45:29 -0500 Subject: [PATCH 24/42] Lint fixes. --- dash/development/validator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dash/development/validator.py b/dash/development/validator.py index 6b7cfd3fd0..2dc5132043 100644 --- a/dash/development/validator.py +++ b/dash/development/validator.py @@ -1,6 +1,7 @@ +from textwrap import dedent import plotly import cerberus -from textwrap import dedent + class DashValidator(cerberus.Validator): types_mapping = cerberus.Validator.types_mapping.copy() From 2c2faf5e8c6a825eafd2e7a3541f1124ec7a6d81 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Mon, 12 Nov 2018 10:31:50 -0500 Subject: [PATCH 25/42] Fixes for review. --- dash/_utils.py | 6 ++ dash/dash.py | 50 +++++++------- dash/development/base_component.py | 69 ++++++++----------- dash/development/validator.py | 4 +- tests/development/metadata_test.py | 2 +- tests/development/test_base_component.py | 2 +- .../development/test_component_validation.py | 2 +- 7 files changed, 63 insertions(+), 72 deletions(-) diff --git a/dash/_utils.py b/dash/_utils.py index 17dc247ebe..730884e69e 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -72,3 +72,9 @@ def first(self, *names): value = self.get(name) if value: return value + + +def _merge(x, y): + z = x.copy() + z.update(y) + return z diff --git a/dash/dash.py b/dash/dash.py index 0013ebbd73..311633bad8 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -11,7 +11,6 @@ import pprint from functools import wraps -from textwrap import dedent import plotly import dash_renderer @@ -65,6 +64,26 @@ _re_index_config_id = re.compile(r'id="_dash-config"') _re_index_scripts_id = re.compile(r'src=".*dash[-_]renderer.*"') +_callback_validation_error_template = """ +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: + +""" # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-locals @@ -927,11 +946,8 @@ def dispatch(self): # 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 - ): + if not self.config.suppress_validation_exceptions and \ + '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']) @@ -956,27 +972,7 @@ def _validate_callback_output(self, namespace, component_type, }) 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( + error_message = _callback_validation_error_template.format( component_property=component_property, component_name=component.__name__, component_id=component_id, diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 34400a0ec3..428b9bda66 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -5,14 +5,34 @@ import abc import sys import pprint -from textwrap import dedent import six -import dash.exceptions +from ..exceptions import ComponentInitializationValidationError +from .._utils import _merge from .validator import DashValidator, generate_validation_error_message from ._all_keywords import kwlist +_initialization_validation_error_callback = """ +A Dash Component was initialized with invalid properties! + +Dash tried to create a `{component_name}` component with the +following arguments, which caused a validation failure: + +*************************************************************** +{component_args} +*************************************************************** + +The expected schema for the `{component_name}` component is: + +*************************************************************** +{component_schema} +*************************************************************** + +The errors in validation are as follows: + + +""" # pylint: disable=no-init,too-few-public-methods class ComponentRegistry: @@ -295,40 +315,20 @@ def validate(self): allow_unknown=True, ) args = { - k: self.__dict__[k] - for k in self.__dict__['_prop_names'] - if k in self.__dict__.keys() + k: v + for k, v in ((x, getattr(self, x, None)) for x in self._prop_names) + if v } valid = validator.validate(args) if not valid: # pylint: disable=protected-access - error_message = dedent("""\ - - A Dash Component was initialized with invalid properties! - - Dash tried to create a `{component_name}` component with the - following arguments, which caused a validation failure: - - *************************************************************** - {component_args} - *************************************************************** - - The expected schema for the `{component_name}` component is: - - *************************************************************** - {component_schema} - *************************************************************** - - The errors in validation are as follows: - - - """).format( + error_message = _initialization_validation_error_callback.format( component_name=self.__class__.__name__, component_args=pprint.pformat(args), component_schema=pprint.pformat(self.__class__._schema) ) - raise dash.exceptions.ComponentInitializationValidationError( + raise ComponentInitializationValidationError( generate_validation_error_message( validator.errors, 0, @@ -383,11 +383,6 @@ def schema_is_nullable(type_object): def js_to_cerberus_type(type_object): - def _merge(x, y): - z = x.copy() - z.update(y) - return z - def _enum(x): schema = {'allowed': [], 'type': ('string', 'number')} @@ -444,13 +439,7 @@ def _enum(x): 'union': lambda x: { 'anyof': [js_to_cerberus_type(v) for v in x['value']], }, - 'any': lambda x: { - 'type': ('boolean', - 'number', - 'string', - 'dict', - 'list') - }, + 'any': lambda x: {}, # Empty means no validation is run 'string': lambda x: {'type': 'string'}, 'bool': lambda x: {'type': 'boolean'}, 'number': lambda x: {'type': 'number'}, @@ -612,7 +601,7 @@ def __repr__(self): else: default_argtext = '' argtext = '**args' - for p in list(props.keys()): + for p in props.keys(): if ( not p.endswith("-*") and # Not a wildcard attribute p not in kwlist and # Not a protected keyword diff --git a/dash/development/validator.py b/dash/development/validator.py index 2dc5132043..fc7614675b 100644 --- a/dash/development/validator.py +++ b/dash/development/validator.py @@ -12,7 +12,7 @@ def _validator_plotly_figure(self, field, value): if not isinstance(value, (dict, plotly.graph_objs.Figure)): self._error( field, - "Invalid Plotly Figure: Not a dict") + "Invalid Plotly Figure.") if isinstance(value, dict): try: plotly.graph_objs.Figure(value) @@ -23,7 +23,7 @@ def _validator_plotly_figure(self, field, value): def _validator_options_with_unique_values(self, field, value): if not isinstance(value, list): - self._error(field, "Invalid options: Not a dict!") + self._error(field, "Invalid options: Not a list!") values = set() for i, option_dict in enumerate(value): if not isinstance(option_dict, dict): diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py index 4ebd819f03..0f57e80a72 100644 --- a/tests/development/metadata_test.py +++ b/tests/development/metadata_test.py @@ -4,7 +4,7 @@ -schema = {'customArrayProp': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'required': False, 'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'required': False, 'type': 'boolean', 'nullable': False}, 'optionalFunc': {'required': False, 'nullable': False}, 'optionalSymbol': {'required': False, 'nullable': False}, 'in': {'required': False, 'type': 'string', 'nullable': False}, 'customProp': {'required': False, 'nullable': False}, 'children': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {'required': False, 'nullable': False}, 'optionalNumber': {'required': False, 'type': 'number', 'nullable': False}, 'optionalObject': {'required': False, 'type': 'dict', 'nullable': False}, 'dashEvents': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'required': False, 'type': 'string', 'nullable': False}, 'optionalString': {'required': False, 'type': 'string', 'nullable': False}, 'optionalElement': {'required': False, 'type': 'component', 'nullable': False}, 'optionalArray': {'required': False, 'type': 'list', 'nullable': False}, 'optionalNode': {'required': False, 'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}], 'nullable': False}, 'optionalObjectOf': {'required': False, 'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {}], 'nullable': False}, 'optionalAny': {'required': False, 'type': ('boolean', 'number', 'string', 'dict', 'list'), 'nullable': False}} +schema = {'customArrayProp': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'required': False, 'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'required': False, 'type': 'boolean', 'nullable': False}, 'optionalFunc': {'required': False, 'nullable': False}, 'optionalSymbol': {'required': False, 'nullable': False}, 'in': {'required': False, 'type': 'string', 'nullable': False}, 'customProp': {'required': False, 'nullable': False}, 'children': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {'required': False, 'nullable': False}, 'optionalNumber': {'required': False, 'type': 'number', 'nullable': False}, 'optionalObject': {'required': False, 'type': 'dict', 'nullable': False}, 'dashEvents': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'required': False, 'type': 'string', 'nullable': False}, 'optionalString': {'required': False, 'type': 'string', 'nullable': False}, 'optionalElement': {'required': False, 'type': 'component', 'nullable': False}, 'optionalArray': {'required': False, 'type': 'list', 'nullable': False}, 'optionalNode': {'required': False, 'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}], 'nullable': False}, 'optionalObjectOf': {'required': False, 'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {}], 'nullable': False}, 'optionalAny': {'required': False, 'nullable': False}} class Table(Component): """A Table component. diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 507ab12160..609c59f962 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -743,7 +743,7 @@ def test_call_signature(self): def test_schema_generation(self): self.assertEqual( self.ComponentClass._schema, - {'customArrayProp': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'required': False, 'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'required': False, 'type': 'boolean', 'nullable': False}, 'optionalFunc': {'required': False, 'nullable': False}, 'optionalSymbol': {'required': False, 'nullable': False}, 'in': {'required': False, 'type': 'string', 'nullable': False}, 'customProp': {'required': False, 'nullable': False}, 'children': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {'required': False, 'nullable': False}, 'optionalNumber': {'required': False, 'type': 'number', 'nullable': False}, 'optionalObject': {'required': False, 'type': 'dict', 'nullable': False}, 'dashEvents': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'required': False, 'type': 'string', 'nullable': False}, 'optionalString': {'required': False, 'type': 'string', 'nullable': False}, 'optionalElement': {'required': False, 'type': 'component', 'nullable': False}, 'optionalArray': {'required': False, 'type': 'list', 'nullable': False}, 'optionalNode': {'required': False, 'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}], 'nullable': False}, 'optionalObjectOf': {'required': False, 'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {}], 'nullable': False}, 'optionalAny': {'required': False, 'type': ('boolean', 'number', 'string', 'dict', 'list'), 'nullable': False}} + {'customArrayProp': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'required': False, 'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'required': False, 'type': 'boolean', 'nullable': False}, 'optionalFunc': {'required': False, 'nullable': False}, 'optionalSymbol': {'required': False, 'nullable': False}, 'in': {'required': False, 'type': 'string', 'nullable': False}, 'customProp': {'required': False, 'nullable': False}, 'children': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {'required': False, 'nullable': False}, 'optionalNumber': {'required': False, 'type': 'number', 'nullable': False}, 'optionalObject': {'required': False, 'type': 'dict', 'nullable': False}, 'dashEvents': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'required': False, 'type': 'string', 'nullable': False}, 'optionalString': {'required': False, 'type': 'string', 'nullable': False}, 'optionalElement': {'required': False, 'type': 'component', 'nullable': False}, 'optionalArray': {'required': False, 'type': 'list', 'nullable': False}, 'optionalNode': {'required': False, 'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}], 'nullable': False}, 'optionalObjectOf': {'required': False, 'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {}], 'nullable': False}, 'optionalAny': {'required': False, 'nullable': False}} ) def test_required_props(self): diff --git a/tests/development/test_component_validation.py b/tests/development/test_component_validation.py index d1f2ce9a79..1262240a46 100644 --- a/tests/development/test_component_validation.py +++ b/tests/development/test_component_validation.py @@ -462,7 +462,7 @@ def test_any_validation(self): self.assertTrue(self.component_validator.validate({ 'optionalAny': {} })) - self.assertFalse(self.component_validator.validate({ + self.assertTrue(self.component_validator.validate({ 'optionalAny': self.ComponentClass() })) self.assertFalse(self.component_validator.validate({ From 5d560e6d4189200168472cd9c17eda1c0c05a2c0 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Mon, 12 Nov 2018 11:56:28 -0500 Subject: [PATCH 26/42] Do not suppress callback exceptions when in `debug=False`. --- dash/dash.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 311633bad8..ca1baa7b1f 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -1147,6 +1147,5 @@ def run_server(self, 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) From 1f4814956dbf6294bef72de077f37fd66102d4d8 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Mon, 12 Nov 2018 12:17:45 -0500 Subject: [PATCH 27/42] Pylint, Flake8 fixes. --- dash/dash.py | 1 + dash/development/base_component.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/dash/dash.py b/dash/dash.py index ca1baa7b1f..23c713bd55 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -85,6 +85,7 @@ """ + # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-locals class Dash(object): diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 428b9bda66..c7d5bb0abd 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -34,6 +34,7 @@ """ + # pylint: disable=no-init,too-few-public-methods class ComponentRegistry: """Holds a registry of the namespaces used by components.""" @@ -314,6 +315,7 @@ def validate(self): self._schema, allow_unknown=True, ) + # pylint: disable=no-member args = { k: v for k, v in ((x, getattr(self, x, None)) for x in self._prop_names) From 29c363ced7a5dee8ef0546f37a81fd6c6beeeab8 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Sat, 17 Nov 2018 18:44:17 -0500 Subject: [PATCH 28/42] Add back circleci flake8 config. --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index a18b9361fb..27996fa64b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,6 +41,8 @@ jobs: . venv/bin/activate pylint dash setup.py --rcfile=$PYLINTRC flake8 dash setup.py + flake8 --ignore=E123,E126,E501,E722,E731,F401,F841,W503,W504 --exclude=metadata_test.py tests + - run: name: Run tests From 783d2c21f9c414ef3471f0ab8bd101d49d47ecfe Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Sat, 17 Nov 2018 18:49:24 -0500 Subject: [PATCH 29/42] Flake8 fixes in tests --- tests/development/test_base_component.py | 2 +- tests/development/test_component_validation.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 609c59f962..61be24c4d7 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -744,7 +744,7 @@ def test_schema_generation(self): self.assertEqual( self.ComponentClass._schema, {'customArrayProp': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'required': False, 'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'required': False, 'type': 'boolean', 'nullable': False}, 'optionalFunc': {'required': False, 'nullable': False}, 'optionalSymbol': {'required': False, 'nullable': False}, 'in': {'required': False, 'type': 'string', 'nullable': False}, 'customProp': {'required': False, 'nullable': False}, 'children': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {'required': False, 'nullable': False}, 'optionalNumber': {'required': False, 'type': 'number', 'nullable': False}, 'optionalObject': {'required': False, 'type': 'dict', 'nullable': False}, 'dashEvents': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'required': False, 'type': 'string', 'nullable': False}, 'optionalString': {'required': False, 'type': 'string', 'nullable': False}, 'optionalElement': {'required': False, 'type': 'component', 'nullable': False}, 'optionalArray': {'required': False, 'type': 'list', 'nullable': False}, 'optionalNode': {'required': False, 'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}], 'nullable': False}, 'optionalObjectOf': {'required': False, 'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {}], 'nullable': False}, 'optionalAny': {'required': False, 'nullable': False}} - ) + ) def test_required_props(self): with self.assertRaises(Exception): diff --git a/tests/development/test_component_validation.py b/tests/development/test_component_validation.py index 1262240a46..14b281d165 100644 --- a/tests/development/test_component_validation.py +++ b/tests/development/test_component_validation.py @@ -16,6 +16,7 @@ html.Div._schema = {'children': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'allowed': [None], 'type': ('string', 'number'), 'nullable': True}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'allowed': [None], 'type': ('string', 'number'), 'nullable': True}], 'nullable': True}}], 'nullable': True}} html.Button._schema = html.Div._schema + class TestComponentValidation(unittest.TestCase): def setUp(self): self.validator = DashValidator From 8e96ed25e67bd638866420a9525b7c9070552993 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Sat, 24 Nov 2018 16:16:41 -0500 Subject: [PATCH 30/42] Lock dev-requirements `dash` version to validation RC. --- dash/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/version.py b/dash/version.py index 9093e4e468..96117b0387 100644 --- a/dash/version.py +++ b/dash/version.py @@ -1 +1 @@ -__version__ = '0.29.0' +__version__ = '0.31.0rc1' From 79b52516dee52266f0c0c89cfd29d768247272c9 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Sat, 24 Nov 2018 19:50:46 -0500 Subject: [PATCH 31/42] Test that sets do not validate as lists. --- tests/development/test_component_validation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/development/test_component_validation.py b/tests/development/test_component_validation.py index 14b281d165..544f9a9fc9 100644 --- a/tests/development/test_component_validation.py +++ b/tests/development/test_component_validation.py @@ -359,6 +359,9 @@ def test_union_validation(self): })) def test_arrayof_validation(self): + self.assertFalse(self.component_validator.validate({ + 'optionalArrayOf': {1, 2, 3} + })) self.assertTrue(self.component_validator.validate({ 'optionalArrayOf': [1, 2, 3] })) From 1bf1036bab391cb870be9a145b36508e850c971a Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Sat, 24 Nov 2018 19:51:20 -0500 Subject: [PATCH 32/42] Change list validation to handle sets properly. --- dash/development/validator.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/dash/development/validator.py b/dash/development/validator.py index fc7614675b..c25468246d 100644 --- a/dash/development/validator.py +++ b/dash/development/validator.py @@ -49,17 +49,19 @@ def _validator_options_with_unique_values(self, field, value): values.add(curr) def _validate_type_list(self, value): - if isinstance(value, list): + if isinstance(value, (list, tuple)): return True - elif isinstance(value, (self.component_class, str)): + # These types can be cast to list + elif isinstance(value, (self.component_class, str, set)): return False + # Handle numpy array / pandas series try: value_list = list(value) - if not isinstance(value_list, list): - return False + if isinstance(value_list, list): + return True except (ValueError, TypeError): - return False - return True + pass + return False # pylint: disable=no-self-use def _validate_type_number(self, value): From d84265156e6f1ec11e70777e9499444b27a2d993 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Sat, 24 Nov 2018 21:17:52 -0500 Subject: [PATCH 33/42] Test that arbitraty types can be validated as a number without error. --- dash/development/validator.py | 4 ++-- tests/development/test_component_validation.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/dash/development/validator.py b/dash/development/validator.py index c25468246d..8c36636d28 100644 --- a/dash/development/validator.py +++ b/dash/development/validator.py @@ -72,12 +72,12 @@ def _validate_type_number(self, value): try: int(value) return True - except (ValueError, TypeError): + except (ValueError, TypeError, AttributeError): pass try: float(value) return True - except (ValueError, TypeError): + except (ValueError, TypeError, AttributeError): pass return False diff --git a/tests/development/test_component_validation.py b/tests/development/test_component_validation.py index 544f9a9fc9..4529cb0c9c 100644 --- a/tests/development/test_component_validation.py +++ b/tests/development/test_component_validation.py @@ -227,6 +227,14 @@ def test_object_validation(self): })) def test_children_validation(self): + + class MyOtherType: + def __init__(self): + pass + + self.assertFalse(self.component_validator.validate({ + 'children': MyOtherType() + })) self.assertTrue(self.component_validator.validate({})) self.assertTrue(self.component_validator.validate({ 'children': None From 5936dfaae90e8e1db06d3c85ef3473d32bf7e2b1 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Fri, 30 Nov 2018 14:02:29 -0500 Subject: [PATCH 34/42] Only print validation suppression message once. --- dash/development/validator.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/dash/development/validator.py b/dash/development/validator.py index 8c36636d28..dc24454064 100644 --- a/dash/development/validator.py +++ b/dash/development/validator.py @@ -69,6 +69,7 @@ def _validate_type_number(self, value): return True if isinstance(value, str): # Since int('3') works return False + # The following handles numpy numeric types try: int(value) return True @@ -90,12 +91,12 @@ def set_component_class(cls, component_cls): cls.types_mapping['dict'] = d_type -def generate_validation_error_message(errors, level=0, error_message=''): +def parse_cerberus_error_tree(errors, level=0, error_message=''): for prop, error_tuple in errors.items(): error_message += (' ' * level) + '* {}'.format(prop) if len(error_tuple) == 2: error_message += '\t<- {}\n'.format(error_tuple[0]) - error_message = generate_validation_error_message( + error_message = parse_cerberus_error_tree( error_tuple[1], level + 1, error_message) @@ -103,12 +104,18 @@ def generate_validation_error_message(errors, level=0, error_message=''): if isinstance(error_tuple[0], str): error_message += '\t<- {}\n'.format(error_tuple[0]) elif isinstance(error_tuple[0], dict): - error_message = generate_validation_error_message( + error_message = parse_cerberus_error_tree( error_tuple[0], level + 1, error_message + "\n") + return error_message + + +def generate_validation_error_message(errors, level=0, error_message=''): + error_message = parse_cerberus_error_tree(errors, level, error_message) error_message += dedent(""" You can turn off these validation exceptions by setting `app.config.suppress_validation_exceptions=True` """) return error_message + \ No newline at end of file From 06d03c78061a6229ac74c5b4d68348056bf1f236 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Fri, 30 Nov 2018 14:58:06 -0500 Subject: [PATCH 35/42] Delete duplicate files from rebase. --- dash/development/_py_components_generation.py | 52 ++-- dash/development/base_component.py | 294 ------------------ dash/development/validator.py | 1 - tests/development/metadata_test.py | 4 - .../development/test_component_validation.py | 6 +- 5 files changed, 33 insertions(+), 324 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index a5d9a4e05e..843b1e0128 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -2,12 +2,15 @@ import copy import os -from dash.development.base_component import _explicitize_args +from dash.development.base_component import ( + _explicitize_args, + generate_property_schema +) from ._all_keywords import kwlist from .base_component import Component -# pylint: disable=unused-argument +# pylint: disable=unused-argument,too-many-locals def generate_class_string(typename, props, description, namespace): """ Dynamically generate class strings to have nicely formatted docstrings, @@ -43,8 +46,12 @@ def generate_class_string(typename, props, description, namespace): # it to be `null` or whether that was just the default value. # The solution might be to deal with default values better although # not all component authors will supply those. - c = '''class {typename}(Component): + c = '''\ +schema = {schema} + +class {typename}(Component): """{docstring}""" + _schema = schema @_explicitize_args def __init__(self, {default_argtext}): self._prop_names = {list_of_valid_keys} @@ -56,18 +63,16 @@ def __init__(self, {default_argtext}): self.available_properties = {list_of_valid_keys} self.available_wildcard_properties =\ {list_of_valid_wildcard_attr_prefixes} - _explicit_args = kwargs.pop('_explicit_args') _locals = locals() _locals.update(kwargs) # For wildcard attrs - args = {{k: _locals[k] for k in _explicit_args if k != 'children'}} - + args = {{k: _locals[k] for k in _explicit_args}} for k in {required_args}: if k not in args: raise TypeError( 'Required argument `' + k + '` was not specified.') + args.pop('children', None) super({typename}, self).__init__({argtext}) - def __repr__(self): if(any(getattr(self, c, None) is not None for c in self._prop_names @@ -108,23 +113,26 @@ def __repr__(self): events = '[' + ', '.join(parse_events(props)) + ']' prop_keys = list(props.keys()) if 'children' in props: - prop_keys.remove('children') - default_argtext = "children=None, " - # pylint: disable=unused-variable - argtext = 'children=children, **args' + default_argtext = 'children=None, ' + argtext = 'children=children, **args' # Children will be popped before else: - default_argtext = "" + default_argtext = '' argtext = '**args' - default_argtext += ", ".join( - [('{:s}=Component.REQUIRED'.format(p) - if props[p]['required'] else - '{:s}=Component.UNDEFINED'.format(p)) - for p in prop_keys - if not p.endswith("-*") and - p not in kwlist and - p not in ['dashEvents', 'fireEvent', 'setProps']] + ['**kwargs'] - ) - + for p in props.keys(): + if ( + not p.endswith("-*") and # Not a wildcard attribute + p not in kwlist and # Not a protected keyword + p not in ['dashEvents', 'fireEvent', 'setProps'] and + p != 'children' # Already accounted for + ): + default_argtext += ('{:s}=Component.REQUIRED, '.format(p) + if props[p]['required'] else + '{:s}=Component.UNDEFINED, '.format(p)) + default_argtext += '**kwargs' + schema = { + k: generate_property_schema(v) + for k, v in props.items() if not k.endswith("-*") + } required_args = required_props(props) return c.format(**locals()) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index b137345e6f..0ed33bd2b1 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -9,7 +9,6 @@ from ..exceptions import ComponentInitializationValidationError from .._utils import _merge from .validator import DashValidator, generate_validation_error_message -from ._all_keywords import kwlist _initialization_validation_error_callback = """ A Dash Component was initialized with invalid properties! @@ -487,296 +486,3 @@ def generate_property_schema(jsonSchema): schema['nullable'] = schema_is_nullable(type_object) schema['required'] = required return schema - - -# pylint: disable=unused-argument -def generate_class_string(typename, props, description, namespace): - """ - Dynamically generate class strings to have nicely formatted docstrings, - keyword arguments, and repr - - Inspired by http://jameso.be/2013/08/06/namedtuple.html - - Parameters - ---------- - typename - props - description - namespace - - Returns - ------- - string - - """ - # TODO _prop_names, _type, _namespace, available_events, - # and available_properties - # can be modified by a Dash JS developer via setattr - # TODO - Tab out the repr for the repr of these components to make it - # look more like a hierarchical tree - # TODO - Include "description" "defaultValue" in the repr and docstring - # - # TODO - Handle "required" - # - # TODO - How to handle user-given `null` values? I want to include - # an expanded docstring like Dropdown(value=None, id=None) - # but by templating in those None values, I have no way of knowing - # whether a property is None because the user explicitly wanted - # it to be `null` or whether that was just the default value. - # The solution might be to deal with default values better although - # not all component authors will supply those. - # pylint: disable=too-many-locals - c = ''' -schema = {schema} - -class {typename}(Component): - """{docstring}""" - _schema = schema - @_explicitize_args - def __init__(self, {default_argtext}): - self._prop_names = {list_of_valid_keys} - self._type = '{typename}' - self._namespace = '{namespace}' - self._valid_wildcard_attributes =\ - {list_of_valid_wildcard_attr_prefixes} - self.available_events = {events} - self.available_properties = {list_of_valid_keys} - self.available_wildcard_properties =\ - {list_of_valid_wildcard_attr_prefixes} - - _explicit_args = kwargs.pop('_explicit_args') - _locals = locals() - _locals.update(kwargs) # For wildcard attrs - args = {{k: _locals[k] for k in _explicit_args}} - - for k in {required_args}: - if k not in args: - raise TypeError( - 'Required argument `' + k + '` was not specified.') - args.pop('children', None) - super({typename}, self).__init__({argtext}) - - def __repr__(self): - if(any(getattr(self, c, None) is not None - for c in self._prop_names - if c is not self._prop_names[0]) - or any(getattr(self, c, None) is not None - for c in self.__dict__.keys() - if any(c.startswith(wc_attr) - for wc_attr in self._valid_wildcard_attributes))): - props_string = ', '.join([c+'='+repr(getattr(self, c, None)) - for c in self._prop_names - if getattr(self, c, None) is not None]) - wilds_string = ', '.join([c+'='+repr(getattr(self, c, None)) - for c in self.__dict__.keys() - if any([c.startswith(wc_attr) - for wc_attr in - self._valid_wildcard_attributes])]) - return ('{typename}(' + props_string + - (', ' + wilds_string if wilds_string != '' else '') + ')') - else: - return ( - '{typename}(' + - repr(getattr(self, self._prop_names[0], None)) + ')') -''' - - filtered_props = reorder_props(filter_props(props)) - # pylint: disable=unused-variable - list_of_valid_wildcard_attr_prefixes = repr(parse_wildcards(props)) - # pylint: disable=unused-variable - list_of_valid_keys = repr(list(map(str, filtered_props.keys()))) - # pylint: disable=unused-variable - docstring = create_docstring( - component_name=typename, - props=filtered_props, - events=parse_events(props), - description=description).replace('\r\n', '\n') - - # pylint: disable=unused-variable - events = '[' + ', '.join(parse_events(props)) + ']' - prop_keys = list(props.keys()) - if 'children' in props: - default_argtext = 'children=None, ' - argtext = 'children=children, **args' # Children will be popped before - else: - default_argtext = '' - argtext = '**args' - for p in props.keys(): - if ( - not p.endswith("-*") and # Not a wildcard attribute - p not in kwlist and # Not a protected keyword - p not in ['dashEvents', 'fireEvent', 'setProps'] and - p != 'children' # Already accounted for - ): - default_argtext += ('{:s}=Component.REQUIRED, '.format(p) - if props[p]['required'] else - '{:s}=Component.UNDEFINED, '.format(p)) - default_argtext += '**kwargs' - schema = { - k: generate_property_schema(v) - for k, v in props.items() if not k.endswith("-*") - } - required_args = required_props(props) - return c.format(**locals()) - - -# pylint: disable=unused-argument -def generate_class_file(typename, props, description, namespace): - """ - Generate a python class file (.py) given a class string - - Parameters - ---------- - typename - props - description - namespace - - Returns - ------- - - """ - import_string =\ - "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + \ - "from dash.development.base_component import " + \ - "Component, _explicitize_args\n\n\n" - class_string = generate_class_string( - typename, - props, - description, - namespace - ) - file_name = "{:s}.py".format(typename) - - file_path = os.path.join(namespace, file_name) - with open(file_path, 'w') as f: - f.write(import_string) - f.write(class_string) - - -# pylint: disable=unused-argument -def generate_class(typename, props, description, namespace): - """ - Generate a python class object given a class string - - Parameters - ---------- - typename - props - description - namespace - - Returns - ------- - - """ - string = generate_class_string(typename, props, description, namespace) - scope = {'Component': Component, '_explicitize_args': _explicitize_args} - # pylint: disable=exec-used - exec(string, scope) - result = scope[typename] - return result - - -def required_props(props): - """ - Pull names of required props from the props object - - Parameters - ---------- - props: dict - - Returns - ------- - list - List of prop names (str) that are required for the Component - """ - return [prop_name for prop_name, prop in list(props.items()) - if prop['required']] - - -def create_docstring(component_name, props, events, description): - """ - Create the Dash component docstring - - Parameters - ---------- - component_name: str - Component name - props: dict - Dictionary with {propName: propMetadata} structure - events: list - List of Dash events - description: str - Component description - - Returns - ------- - str - Dash component docstring - """ - # Ensure props are ordered with children first - props = reorder_props(props=props) - - return ( - """A {name} component.\n{description} - -Keyword arguments:\n{args} - -Available events: {events}""" - ).format( - name=component_name, - description=description, - args='\n'.join( - create_prop_docstring( - prop_name=p, - type_object=prop['type'] if 'type' in prop - else prop['flowType'], - required=prop['required'], - description=prop['description'], - indent_num=0, - is_flow_type='flowType' in prop and 'type' not in prop) - for p, prop in list(filter_props(props).items())), - events=', '.join(events)) - - -def parse_events(props): - """ - Pull out the dashEvents from the Component props - - Parameters - ---------- - props: dict - Dictionary with {propName: propMetadata} structure - - Returns - ------- - list - List of Dash event strings - """ - if 'dashEvents' in props and props['dashEvents']['type']['name'] == 'enum': - events = [v['value'] for v in props['dashEvents']['type']['value']] - else: - varnames = func.__code__.co_varnames - - def wrapper(*args, **kwargs): - if '_explicit_args' in kwargs.keys(): - raise Exception('Variable _explicit_args should not be set.') - kwargs['_explicit_args'] = \ - list( - set( - list(varnames[:len(args)]) + [k for k, _ in kwargs.items()] - ) - ) - if 'self' in kwargs['_explicit_args']: - kwargs['_explicit_args'].remove('self') - return func(*args, **kwargs) - - # If Python 3, we can set the function signature to be correct - if hasattr(inspect, 'signature'): - # pylint: disable=no-member - new_sig = inspect.signature(wrapper).replace( - parameters=inspect.signature(func).parameters.values() - ) - wrapper.__signature__ = new_sig - return wrapper diff --git a/dash/development/validator.py b/dash/development/validator.py index dc24454064..5bd2795703 100644 --- a/dash/development/validator.py +++ b/dash/development/validator.py @@ -118,4 +118,3 @@ def generate_validation_error_message(errors, level=0, error_message=''): `app.config.suppress_validation_exceptions=True` """) return error_message - \ No newline at end of file diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py index 0f57e80a72..6c8b8455cd 100644 --- a/tests/development/metadata_test.py +++ b/tests/development/metadata_test.py @@ -3,7 +3,6 @@ from dash.development.base_component import Component, _explicitize_args - schema = {'customArrayProp': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'required': False, 'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'required': False, 'type': 'boolean', 'nullable': False}, 'optionalFunc': {'required': False, 'nullable': False}, 'optionalSymbol': {'required': False, 'nullable': False}, 'in': {'required': False, 'type': 'string', 'nullable': False}, 'customProp': {'required': False, 'nullable': False}, 'children': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {'required': False, 'nullable': False}, 'optionalNumber': {'required': False, 'type': 'number', 'nullable': False}, 'optionalObject': {'required': False, 'type': 'dict', 'nullable': False}, 'dashEvents': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'required': False, 'type': 'string', 'nullable': False}, 'optionalString': {'required': False, 'type': 'string', 'nullable': False}, 'optionalElement': {'required': False, 'type': 'component', 'nullable': False}, 'optionalArray': {'required': False, 'type': 'list', 'nullable': False}, 'optionalNode': {'required': False, 'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}], 'nullable': False}, 'optionalObjectOf': {'required': False, 'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {}], 'nullable': False}, 'optionalAny': {'required': False, 'nullable': False}} class Table(Component): @@ -51,19 +50,16 @@ def __init__(self, children=None, optionalArray=Component.UNDEFINED, optionalBoo self.available_events = ['restyle', 'relayout', 'click'] self.available_properties = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'data-*', 'aria-*', 'customProp', 'customArrayProp', 'in', 'id'] self.available_wildcard_properties = ['data-', 'aria-'] - _explicit_args = kwargs.pop('_explicit_args') _locals = locals() _locals.update(kwargs) # For wildcard attrs args = {k: _locals[k] for k in _explicit_args} - for k in []: if k not in args: raise TypeError( 'Required argument `' + k + '` was not specified.') args.pop('children', None) super(Table, self).__init__(children=children, **args) - def __repr__(self): if(any(getattr(self, c, None) is not None for c in self._prop_names diff --git a/tests/development/test_component_validation.py b/tests/development/test_component_validation.py index 4529cb0c9c..c9061abdd3 100644 --- a/tests/development/test_component_validation.py +++ b/tests/development/test_component_validation.py @@ -5,12 +5,12 @@ import numpy as np import pandas as pd import plotly.graph_objs as go -import dash import dash_html_components as html -from importlib import import_module +import dash from dash.development.component_loader import _get_metadata -from dash.development.base_component import generate_class, Component +from dash.development.base_component import Component from dash.development.validator import DashValidator +from dash.development._py_components_generation import generate_class # Monkey patched html html.Div._schema = {'children': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'allowed': [None], 'type': ('string', 'number'), 'nullable': True}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'allowed': [None], 'type': ('string', 'number'), 'nullable': True}], 'nullable': True}}], 'nullable': True}} From 83f501a1766ffb98a77725b246c604a201d96339 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Fri, 30 Nov 2018 15:04:07 -0500 Subject: [PATCH 36/42] Properly remove schema for python version cross compatibility. --- tests/development/test_base_component.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index d7a371647d..8da103dc8f 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -542,7 +542,8 @@ def setUp(self): def remove_schema(string): tmp = string.split("\n") - return "\n".join(tmp[:6] + tmp[7:]) + return "\n".join(tmp[:5] + tmp[6:]) + self.expected_class_string = remove_schema(self.expected_class_string) self.component_class_string =\ remove_schema(self.component_class_string) From 1dae8125544ac1f0143eb4b3bbf83180ca0fb54a Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Fri, 30 Nov 2018 15:55:59 -0500 Subject: [PATCH 37/42] Fix prop filtering in validate method --- dash/development/base_component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 0ed33bd2b1..5006f9add8 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -316,7 +316,7 @@ def validate(self): args = { k: v for k, v in ((x, getattr(self, x, None)) for x in self._prop_names) - if v + if v is not None } valid = validator.validate(args) if not valid: From 7d00b80da3414991307f010d46bb547f8851a578 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Fri, 30 Nov 2018 15:57:42 -0500 Subject: [PATCH 38/42] :hocho: old method of validating required props --- dash/development/_py_components_generation.py | 22 ------------------- tests/development/metadata_test.py | 4 ---- tests/development/test_base_component.py | 6 ++--- 3 files changed, 3 insertions(+), 29 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index 843b1e0128..7e449e02e4 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -67,10 +67,6 @@ def __init__(self, {default_argtext}): _locals = locals() _locals.update(kwargs) # For wildcard attrs args = {{k: _locals[k] for k in _explicit_args}} - for k in {required_args}: - if k not in args: - raise TypeError( - 'Required argument `' + k + '` was not specified.') args.pop('children', None) super({typename}, self).__init__({argtext}) def __repr__(self): @@ -133,7 +129,6 @@ def __repr__(self): k: generate_property_schema(v) for k, v in props.items() if not k.endswith("-*") } - required_args = required_props(props) return c.format(**locals()) @@ -224,23 +219,6 @@ def generate_class(typename, props, description, namespace): return result -def required_props(props): - """ - Pull names of required props from the props object - - Parameters - ---------- - props: dict - - Returns - ------- - list - List of prop names (str) that are required for the Component - """ - return [prop_name for prop_name, prop in list(props.items()) - if prop['required']] - - def create_docstring(component_name, props, events, description): """ Create the Dash component docstring diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py index 6c8b8455cd..dc1609fc33 100644 --- a/tests/development/metadata_test.py +++ b/tests/development/metadata_test.py @@ -54,10 +54,6 @@ def __init__(self, children=None, optionalArray=Component.UNDEFINED, optionalBoo _locals = locals() _locals.update(kwargs) # For wildcard attrs args = {k: _locals[k] for k in _explicit_args} - for k in []: - if k not in args: - raise TypeError( - 'Required argument `' + k + '` was not specified.') args.pop('children', None) super(Table, self).__init__(children=children, **args) def __repr__(self): diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 8da103dc8f..b52d8518e9 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -744,12 +744,12 @@ def test_schema_generation(self): def test_required_props(self): with self.assertRaises(Exception): - self.ComponentClassRequired() + self.ComponentClassRequired().validate() self.ComponentClassRequired(id='test') with self.assertRaises(Exception): - self.ComponentClassRequired(id='test', lahlah='test') + self.ComponentClassRequired(id='test', lahlah='test').validate() with self.assertRaises(Exception): - self.ComponentClassRequired(children='test') + self.ComponentClassRequired(children='test').validate() class TestMetaDataConversions(unittest.TestCase): From 792ef39fc8939dbc3ec0969bda069cf8cf5a2d2a Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 21 Feb 2019 18:29:20 -0500 Subject: [PATCH 39/42] Add test_component_validation test to test.sh --- test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/test.sh b/test.sh index 48f97a1cc5..e097278cb9 100755 --- a/test.sh +++ b/test.sh @@ -2,6 +2,7 @@ EXIT_STATE=0 python -m unittest tests.development.test_base_component || EXIT_STATE=$? python -m unittest tests.development.test_component_loader || EXIT_STATE=$? +python -m unittest tests.development.test_component_validation || EXIT_STATE=$? python -m unittest tests.test_integration || EXIT_STATE=$? python -m unittest tests.test_resources || EXIT_STATE=$? python -m unittest tests.test_configs || EXIT_STATE=$? From 598a4326cc1a4047ae8b20e3e6674481b5703cf4 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 21 Feb 2019 18:29:54 -0500 Subject: [PATCH 40/42] Add generated schema to new base class, fix tests. --- dash/development/_py_components_generation.py | 7 ++++++- tests/development/metadata_test.py | 13 +++++++++---- tests/development/test_base_component.py | 6 ------ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py index de4e938ed9..59ae91512d 100644 --- a/dash/development/_py_components_generation.py +++ b/dash/development/_py_components_generation.py @@ -103,6 +103,10 @@ def __init__(self, {default_argtext}): p != 'setProps'] + ["**kwargs"] ) required_args = required_props(props) + schema = { + k: generate_property_schema(v) + for k, v in props.items() if not k.endswith("-*") + } return c.format( typename=typename, namespace=namespace, @@ -112,7 +116,8 @@ def __init__(self, {default_argtext}): docstring=docstring, default_argtext=default_argtext, argtext=argtext, - required_props=required_args + required_props=required_args, + schema=schema ) diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py index b3ea80ca63..ee0267d620 100644 --- a/tests/development/metadata_test.py +++ b/tests/development/metadata_test.py @@ -3,7 +3,7 @@ from dash.development.base_component import Component, _explicitize_args -schema = {'customArrayProp': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'required': False, 'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'required': False, 'type': 'boolean', 'nullable': False}, 'optionalFunc': {'required': False, 'nullable': False}, 'optionalSymbol': {'required': False, 'nullable': False}, 'in': {'required': False, 'type': 'string', 'nullable': False}, 'customProp': {'required': False, 'nullable': False}, 'children': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {'required': False, 'nullable': False}, 'optionalNumber': {'required': False, 'type': 'number', 'nullable': False}, 'optionalObject': {'required': False, 'type': 'dict', 'nullable': False}, 'dashEvents': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'required': False, 'type': 'string', 'nullable': False}, 'optionalString': {'required': False, 'type': 'string', 'nullable': False}, 'optionalElement': {'required': False, 'type': 'component', 'nullable': False}, 'optionalArray': {'required': False, 'type': 'list', 'nullable': False}, 'optionalNode': {'required': False, 'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}], 'nullable': False}, 'optionalObjectOf': {'required': False, 'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {}], 'nullable': False}, 'optionalAny': {'required': False, 'nullable': False}} +schema = {'in': {'required': False, 'type': 'string', 'nullable': False}, 'optionalObjectWithShapeAndNestedDescription': {'required': False, 'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalNode': {'required': False, 'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}], 'nullable': False}, 'optionalSymbol': {'required': False, 'nullable': False}, 'optionalMessage': {'required': False, 'nullable': False}, 'optionalString': {'required': False, 'type': 'string', 'nullable': False}, 'optionalObjectOf': {'required': False, 'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalObject': {'required': False, 'type': 'dict', 'nullable': False}, 'children': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalEnum': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalBool': {'required': False, 'type': 'boolean', 'nullable': False}, 'optionalNumber': {'required': False, 'type': 'number', 'nullable': False}, 'optionalFunc': {'required': False, 'nullable': False}, 'optionalElement': {'required': False, 'type': 'component', 'nullable': False}, 'id': {'required': False, 'type': 'string', 'nullable': False}, 'optionalArray': {'required': False, 'type': 'list', 'nullable': False}, 'optionalAny': {'required': False, 'nullable': False}, 'customProp': {'required': False, 'nullable': False}, 'optionalUnion': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {}], 'nullable': False}, 'customArrayProp': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'nullable': False}}, 'optionalArrayOf': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'type': 'number', 'nullable': False}}} class Table(Component): """A Table component. @@ -38,17 +38,22 @@ class Table(Component): - customArrayProp (list; optional) - in (string; optional) - id (string; optional)""" + _schema = schema @_explicitize_args def __init__(self, children=None, optionalArray=Component.UNDEFINED, optionalBool=Component.UNDEFINED, optionalFunc=Component.UNDEFINED, optionalNumber=Component.UNDEFINED, optionalObject=Component.UNDEFINED, optionalString=Component.UNDEFINED, optionalSymbol=Component.UNDEFINED, optionalNode=Component.UNDEFINED, optionalElement=Component.UNDEFINED, optionalMessage=Component.UNDEFINED, optionalEnum=Component.UNDEFINED, optionalUnion=Component.UNDEFINED, optionalArrayOf=Component.UNDEFINED, optionalObjectOf=Component.UNDEFINED, optionalObjectWithShapeAndNestedDescription=Component.UNDEFINED, optionalAny=Component.UNDEFINED, customProp=Component.UNDEFINED, customArrayProp=Component.UNDEFINED, id=Component.UNDEFINED, **kwargs): self._prop_names = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'data-*', 'aria-*', 'customProp', 'customArrayProp', 'in', 'id'] self._type = 'Table' self._namespace = 'TableComponents' self._valid_wildcard_attributes = ['data-', 'aria-'] - self.available_properties = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', 'in', 'id'] + self.available_properties = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'data-*', 'aria-*', 'customProp', 'customArrayProp', 'in', 'id'] self.available_wildcard_properties = ['data-', 'aria-'] _explicit_args = kwargs.pop('_explicit_args') _locals = locals() _locals.update(kwargs) # For wildcard attrs - args = {k: _locals[k] for k in _explicit_args} - args.pop('children', None) + args = {k: _locals[k] for k in _explicit_args if k != 'children'} + + for k in []: + if k not in args: + raise TypeError( + 'Required argument `' + k + '` was not specified.') super(Table, self).__init__(children=children, **args) diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 6ef3db5ba8..4551daac1a 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -743,12 +743,6 @@ def test_call_signature(self): ['None'] + ['undefined'] * 19 ) - def test_schema_generation(self): - self.assertEqual( - self.ComponentClass._schema, - {'customArrayProp': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'nullable': False}}, 'optionalObjectWithShapeAndNestedDescription': {'required': False, 'nullable': False, 'type': 'dict', 'allow_unknown': False, 'schema': {'color': {'type': 'string'}, 'fontSize': {'type': 'number'}, 'figure': {'schema': {'layout': {'type': 'dict'}, 'data': {'type': 'list', 'schema': {'type': 'dict', 'nullable': False}}}, 'type': 'dict', 'allow_unknown': False, 'nullable': False}}}, 'optionalBool': {'required': False, 'type': 'boolean', 'nullable': False}, 'optionalFunc': {'required': False, 'nullable': False}, 'optionalSymbol': {'required': False, 'nullable': False}, 'in': {'required': False, 'type': 'string', 'nullable': False}, 'customProp': {'required': False, 'nullable': False}, 'children': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}, {'type': 'list', 'schema': {'anyof': [{'type': 'string'}, {'type': 'number'}, {'type': 'boolean'}, {'type': 'component'}, {'nullable': True, 'type': ('string', 'number'), 'allowed': [None]}], 'nullable': True}}], 'nullable': True}, 'optionalMessage': {'required': False, 'nullable': False}, 'optionalNumber': {'required': False, 'type': 'number', 'nullable': False}, 'optionalObject': {'required': False, 'type': 'dict', 'nullable': False}, 'dashEvents': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['restyle', 'relayout', 'click']}, 'id': {'required': False, 'type': 'string', 'nullable': False}, 'optionalString': {'required': False, 'type': 'string', 'nullable': False}, 'optionalElement': {'required': False, 'type': 'component', 'nullable': False}, 'optionalArray': {'required': False, 'type': 'list', 'nullable': False}, 'optionalNode': {'required': False, 'anyof': [{'type': 'component'}, {'type': 'boolean'}, {'type': 'number'}, {'type': 'string'}, {'type': 'list', 'schema': {'type': ('component', 'boolean', 'number', 'string')}}], 'nullable': False}, 'optionalObjectOf': {'required': False, 'type': 'dict', 'valueschema': {'type': 'number'}, 'nullable': False}, 'optionalEnum': {'required': False, 'nullable': False, 'type': ('string', 'number'), 'allowed': ['News', 'Photos', '1', 1, 1.0, '2', 2, 2.0, False, True]}, 'optionalArrayOf': {'required': False, 'nullable': False, 'type': 'list', 'schema': {'type': 'number', 'nullable': False}}, 'optionalUnion': {'required': False, 'anyof': [{'type': 'string'}, {'type': 'number'}, {}], 'nullable': False}, 'optionalAny': {'required': False, 'nullable': False}} - ) - def test_required_props(self): with self.assertRaises(Exception): self.ComponentClassRequired().validate() From aa49488000ebe7303e2162253ce6794d6f7015b1 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 21 Feb 2019 18:38:02 -0500 Subject: [PATCH 41/42] Validation tests require numpy and pandas. --- .circleci/requirements/dev-requirements-py37.txt | 2 ++ .circleci/requirements/dev-requirements.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.circleci/requirements/dev-requirements-py37.txt b/.circleci/requirements/dev-requirements-py37.txt index c667497045..67d4afa559 100644 --- a/.circleci/requirements/dev-requirements-py37.txt +++ b/.circleci/requirements/dev-requirements-py37.txt @@ -15,3 +15,5 @@ flake8 pylint==2.2.2 astroid==2.1.0 Cerberus +numpy +pandas diff --git a/.circleci/requirements/dev-requirements.txt b/.circleci/requirements/dev-requirements.txt index 6a79300fb5..3e6a820253 100644 --- a/.circleci/requirements/dev-requirements.txt +++ b/.circleci/requirements/dev-requirements.txt @@ -15,3 +15,5 @@ requests[security] flake8 pylint==1.9.4 Cerberus +numpy +pandas From 56c28730ea7a6fec26ceae5e8132eff3c3d05748 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 21 Feb 2019 18:46:33 -0500 Subject: [PATCH 42/42] Remove duplicate key --- tests/development/test_component_loader.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/development/test_component_loader.py b/tests/development/test_component_loader.py index efc656b5d8..97babc26d8 100644 --- a/tests/development/test_component_loader.py +++ b/tests/development/test_component_loader.py @@ -198,7 +198,6 @@ def test_loadcomponents(self): 'baz': 'Lemons', 'data-foo': 'Blah', 'aria-bar': 'Seven', - 'baz': 'Lemons', 'children': 'Child' } AKwargs = {