From a27a5b7443561475ed1de6111c8b30e03d5880fd Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Fri, 8 Jun 2018 22:30:56 -0400 Subject: [PATCH 01/14] generate_class_string and generate_class_file functions with tests. --- dash/development/base_component.py | 65 +++++++++++++++++++-- tests/development/metadata_test.py | 72 ++++++++++++++++++++++++ tests/development/test_base_component.py | 54 ++++++++++++++++++ 3 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 tests/development/metadata_test.py diff --git a/dash/development/base_component.py b/dash/development/base_component.py index d647df2411..51238d80a9 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -1,5 +1,7 @@ import collections import copy +import os +import errno def is_number(s): @@ -200,9 +202,9 @@ def __len__(self): # pylint: disable=unused-argument -def generate_class(typename, props, description, namespace): +def generate_class_string(typename, props, description, namespace): """ - Dynamically generate classes to have nicely formatted docstrings, + Dynamically generate class strings to have nicely formatted docstrings, keyword arguments, and repr Inspired by http://jameso.be/2013/08/06/namedtuple.html @@ -216,6 +218,7 @@ def generate_class(typename, props, description, namespace): Returns ------- + string """ # TODO _prop_names, _type, _namespace, available_events, @@ -276,13 +279,13 @@ def __repr__(self): 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(filtered_props.keys())) + list_of_valid_keys = repr(list(map(str, filtered_props.keys()))) # pylint: disable=unused-variable docstring = create_docstring( component_name=typename, @@ -302,9 +305,61 @@ def __repr__(self): 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 + ------- + + """ + string = generate_class_string(typename, props, description, namespace) + file_name = "{:s}.py".format(typename) + + # Convoluted way since os.makedirs(..., exist_ok=True) only >=3.2 + try: + os.makedirs(namespace) + except OSError as exc: # Python >2.5 + if exc.errno == errno.EEXIST and os.path.isdir(namespace): + pass + else: + raise + file_path = os.path.join(namespace, file_name) + with open(file_path, 'w') as f: + f.write(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} # pylint: disable=exec-used - exec(c.format(**locals()), scope) + exec(string, scope) result = scope[typename] return result diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py new file mode 100644 index 0000000000..68a8edeb18 --- /dev/null +++ b/tests/development/metadata_test.py @@ -0,0 +1,72 @@ +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) +- optionalArray (list; optional): Description of optionalArray +- optionalBool (boolean; optional) +- optionalNumber (number; optional) +- optionalObject (dict; optional) +- 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) +- optionalUnion (string | number; optional) +- optionalArrayOf (list; optional) +- optionalObjectOf (dict with strings as keys and values of type number; optional) +- optionalObjectWithShapeAndNestedDescription (optional): . optionalObjectWithShapeAndNestedDescription has the following type: dict containing keys 'color', 'fontSize', 'figure'. +Those keys have the following types: + - color (string; optional) + - fontSize (number; optional) + - figure (optional): Figure is a plotly graph object. figure has the following type: dict containing keys 'data', 'layout'. +Those keys have the following types: + - 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) +- id (string; optional) + +Available events: 'restyle', 'relayout', 'click'""" + def __init__(self, children=None, **kwargs): + self._prop_names = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', '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-*', 'id'] + self.available_wildcard_properties = ['data-', 'aria-'] + + for k in []: + if k not in kwargs: + raise TypeError( + 'Required argument `' + k + '` was not specified.') + + super(Table, self).__init__(children=children, **kwargs) + + 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 ('Table(' + props_string + + (', ' + wilds_string if wilds_string != '' else '') + ')') + else: + return ( + 'Table(' + + repr(getattr(self, self._prop_names[0], None)) + ')') diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 9b4b3b609b..9137fa37df 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -8,6 +8,8 @@ from dash.development.base_component import ( generate_class, + generate_class_string, + generate_class_file, Component, js_to_py_type, create_docstring, @@ -488,6 +490,58 @@ def test_pop(self): self.assertTrue(c2_popped is c2) +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 + + # Class string generated from generate_class_string + self.component_class_string = generate_class_string( + typename='Table', + props=data['props'], + description=data['description'], + namespace='TableComponents' + ) + + # Class string written to file + generate_class_file( + typename='Table', + props=data['props'], + description=data['description'], + namespace='TableComponents' + ) + written_file_path = os.path.join( + 'TableComponents', "Table.py" + ) + with open(written_file_path, 'r') as f: + self.written_class_string = f.read() + + # The expected result for both class string and class file generation + expected_string_path = os.path.join( + 'tests', 'development', 'metadata_test.py' + ) + with open(expected_string_path, 'r') as f: + self.expected_class_string = f.read() + + + def test_class_string(self): + self.assertEqual( + self.expected_class_string, + self.component_class_string + ) + + def test_class_file(self): + self.assertEqual( + self.expected_class_string, + self.written_class_string + ) + + class TestGenerateClass(unittest.TestCase): def setUp(self): path = os.path.join('tests', 'development', 'metadata_test.json') From e79214afd2e370aa55a19f24e0d8696fc6bf65fb Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Sun, 10 Jun 2018 16:54:36 -0400 Subject: [PATCH 02/14] Add generate_classes function. --- dash/development/component_loader.py | 52 ++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index cb82a609e9..5ef846046f 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -1,6 +1,17 @@ import collections import json from .base_component import generate_class +from .base_component import generate_class_file + + +def _get_metadata(metadata_path): + # Start processing + with open(metadata_path) as data_file: + json_string = data_file.read() + data = json\ + .JSONDecoder(object_pairs_hook=collections.OrderedDict)\ + .decode(json_string) + return data def load_components(metadata_path, @@ -20,12 +31,7 @@ def load_components(metadata_path, components = [] - # Start processing - with open(metadata_path) as data_file: - json_string = data_file.read() - data = json\ - .JSONDecoder(object_pairs_hook=collections.OrderedDict)\ - .decode(json_string) + data = _get_metadata(metadata_path) # Iterate over each property name (which is a path to the component) for componentPath in data: @@ -47,3 +53,37 @@ def load_components(metadata_path, components.append(component) return components + + +def generate_classes(metadata_path, + namespace='default_namespace'): + """Load React component metadata into a format Dash can parse, + then create python class files. + + Usage: generate_classes('../../component-suites/lib/metadata.json') + + Keyword arguments: + metadata_path -- a path to a JSON file created by + [`react-docgen`](https://github.com/reactjs/react-docgen). + + Returns: + """ + + data = _get_metadata(metadata_path) + + # Iterate over each property name (which is a path to the component) + for componentPath in data: + componentData = data[componentPath] + + # Extract component name from path + # e.g. src/components/MyControl.react.js + # TODO Make more robust - some folks will write .jsx and others + # will be on windows. Unfortunately react-docgen doesn't include + # the name of the component atm. + name = componentPath.split('/').pop().split('.')[0] + generate_class_file( + name, + componentData['props'], + componentData['description'], + namespace + ) From 3812d8db5bce10b0ca3526ea9e81b93280a151b4 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Mon, 11 Jun 2018 19:17:58 -0400 Subject: [PATCH 03/14] generate_classes tests. --- dash/development/base_component.py | 21 +++--- tests/development/metadata_test.py | 3 + tests/development/test_base_component.py | 18 ++++-- tests/development/test_component_loader.py | 75 +++++++++++++++++++++- 4 files changed, 100 insertions(+), 17 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 51238d80a9..0abd500514 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -1,7 +1,6 @@ import collections import copy import os -import errno def is_number(s): @@ -324,20 +323,20 @@ def generate_class_file(typename, props, description, namespace): ------- """ - string = generate_class_string(typename, props, description, namespace) + import_string =\ + "from dash.development.base_component import Component\n\n\n" + class_string = generate_class_string( + typename, + props, + description, + namespace + ) file_name = "{:s}.py".format(typename) - # Convoluted way since os.makedirs(..., exist_ok=True) only >=3.2 - try: - os.makedirs(namespace) - except OSError as exc: # Python >2.5 - if exc.errno == errno.EEXIST and os.path.isdir(namespace): - pass - else: - raise file_path = os.path.join(namespace, file_name) with open(file_path, 'w') as f: - f.write(string) + f.write(import_string) + f.write(class_string) # pylint: disable=unused-argument diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py index 68a8edeb18..427e677890 100644 --- a/tests/development/metadata_test.py +++ b/tests/development/metadata_test.py @@ -1,3 +1,6 @@ +from dash.development.base_component import Component + + class Table(Component): """A Table component. This is a description of the component. diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 9137fa37df..70aab9eab5 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -3,6 +3,7 @@ import inspect import json import os +import shutil import unittest import plotly @@ -500,8 +501,15 @@ def setUp(self): .decode(json_string) self.data = data + # Create a folder for the new component file + os.makedirs('TableComponents') + + # Import string not included in generated class string + import_string =\ + "from dash.development.base_component import Component\n\n\n" + # Class string generated from generate_class_string - self.component_class_string = generate_class_string( + self.component_class_string = import_string + generate_class_string( typename='Table', props=data['props'], description=data['description'], @@ -520,7 +528,7 @@ def setUp(self): ) with open(written_file_path, 'r') as f: self.written_class_string = f.read() - + # The expected result for both class string and class file generation expected_string_path = os.path.join( 'tests', 'development', 'metadata_test.py' @@ -528,6 +536,8 @@ def setUp(self): with open(expected_string_path, 'r') as f: self.expected_class_string = f.read() + def tearDown(self): + shutil.rmtree('TableComponents') def test_class_string(self): self.assertEqual( @@ -537,8 +547,8 @@ def test_class_string(self): def test_class_file(self): self.assertEqual( - self.expected_class_string, - self.written_class_string + self.expected_class_string[:100], + self.written_class_string[:100] ) diff --git a/tests/development/test_component_loader.py b/tests/development/test_component_loader.py index 3748705a90..ba36b8a081 100644 --- a/tests/development/test_component_loader.py +++ b/tests/development/test_component_loader.py @@ -1,9 +1,14 @@ import collections import json import os +import shutil import unittest -from dash.development.component_loader import load_components -from dash.development.base_component import generate_class, Component +from dash.development.component_loader import load_components, generate_classes +from dash.development.base_component import ( + generate_class, + generate_class_file, + Component +) METADATA_PATH = 'metadata.json' @@ -150,3 +155,69 @@ def test_loadcomponents(self): repr(A(**AKwargs)), repr(c[1](**AKwargs)) ) + + +class TestGenerateClasses(unittest.TestCase): + def setUp(self): + with open(METADATA_PATH, 'w') as f: + f.write(METADATA_STRING) + os.makedirs('default_namespace') + + init_file_path = 'default_namespace/__init__.py' + with open(init_file_path, 'a'): + os.utime(init_file_path, None) + + def tearDown(self): + os.remove(METADATA_PATH) + shutil.rmtree('default_namespace') + + def test_loadcomponents(self): + MyComponent_runtime = generate_class( + 'MyComponent', + METADATA['MyComponent.react.js']['props'], + METADATA['MyComponent.react.js']['description'], + 'default_namespace' + ) + + A_runtime = generate_class( + 'A', + METADATA['A.react.js']['props'], + METADATA['A.react.js']['description'], + 'default_namespace' + ) + + generate_classes(METADATA_PATH) + from default_namespace.MyComponent import MyComponent \ + as MyComponent_buildtime + from default_namespace.A import A as A_buildtime + + MyComponentKwargs = { + 'foo': 'Hello World', + 'bar': 'Lah Lah', + 'baz': 'Lemons', + 'data-foo': 'Blah', + 'aria-bar': 'Seven', + 'baz': 'Lemons', + 'children': 'Child' + } + AKwargs = { + 'children': 'Child', + 'href': 'Hello World' + } + + self.assertTrue( + isinstance( + MyComponent_buildtime(**MyComponentKwargs), + Component + ) + ) + + self.assertEqual( + repr(MyComponent_buildtime(**MyComponentKwargs)), + repr(MyComponent_runtime(**MyComponentKwargs)), + ) + + self.assertEqual( + repr(A_runtime(**AKwargs)), + repr(A_buildtime(**AKwargs)) + ) From 03288a163368928e821d5376eb9c321e45397aa4 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Mon, 11 Jun 2018 23:05:21 -0400 Subject: [PATCH 04/14] update generate_classes and tests to automatically look in lib/ --- dash/development/component_loader.py | 7 ++++--- tests/development/test_base_component.py | 4 ++-- tests/development/test_component_loader.py | 3 +-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 5ef846046f..00479b6125 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -55,8 +55,7 @@ def load_components(metadata_path, return components -def generate_classes(metadata_path, - namespace='default_namespace'): +def generate_classes(metadata_path='lib/metadata.json', output_dir='lib'): """Load React component metadata into a format Dash can parse, then create python class files. @@ -66,6 +65,8 @@ def generate_classes(metadata_path, metadata_path -- a path to a JSON file created by [`react-docgen`](https://github.com/reactjs/react-docgen). + output_dir -- the directory to put component classes in + Returns: """ @@ -85,5 +86,5 @@ def generate_classes(metadata_path, name, componentData['props'], componentData['description'], - namespace + output_dir ) diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 70aab9eab5..90c64f7616 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -547,8 +547,8 @@ def test_class_string(self): def test_class_file(self): self.assertEqual( - self.expected_class_string[:100], - self.written_class_string[:100] + self.expected_class_string, + self.written_class_string ) diff --git a/tests/development/test_component_loader.py b/tests/development/test_component_loader.py index ba36b8a081..cff677b603 100644 --- a/tests/development/test_component_loader.py +++ b/tests/development/test_component_loader.py @@ -6,7 +6,6 @@ from dash.development.component_loader import load_components, generate_classes from dash.development.base_component import ( generate_class, - generate_class_file, Component ) @@ -186,7 +185,7 @@ def test_loadcomponents(self): 'default_namespace' ) - generate_classes(METADATA_PATH) + generate_classes(METADATA_PATH, 'default_namespace') from default_namespace.MyComponent import MyComponent \ as MyComponent_buildtime from default_namespace.A import A as A_buildtime From f761537fa996b77d4adf5c14d386574485e6f291 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Wed, 13 Jun 2018 07:17:06 -0400 Subject: [PATCH 05/14] Small change to generate_classes docstring. --- dash/development/component_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 00479b6125..51972588ee 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -59,7 +59,7 @@ def generate_classes(metadata_path='lib/metadata.json', output_dir='lib'): """Load React component metadata into a format Dash can parse, then create python class files. - Usage: generate_classes('../../component-suites/lib/metadata.json') + Usage: generate_classes() Keyword arguments: metadata_path -- a path to a JSON file created by From 4cc4ad1f8f70515e06a76ef5d5e6dddc57302357 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Tue, 19 Jun 2018 20:20:20 -0400 Subject: [PATCH 06/14] Added 'AUTO GENERATED FILE - DO NOT EDIT' comment. --- dash/development/base_component.py | 1 + tests/development/metadata_test.py | 2 ++ tests/development/test_base_component.py | 1 + 3 files changed, 4 insertions(+) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 0abd500514..bda33b787c 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -324,6 +324,7 @@ def generate_class_file(typename, props, description, namespace): """ import_string =\ + "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + \ "from dash.development.base_component import Component\n\n\n" class_string = generate_class_string( typename, diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py index 427e677890..b25364e559 100644 --- a/tests/development/metadata_test.py +++ b/tests/development/metadata_test.py @@ -1,3 +1,5 @@ +# AUTO GENERATED FILE - DO NOT EDIT + from dash.development.base_component import Component diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 90c64f7616..cab2b0a36e 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -506,6 +506,7 @@ def setUp(self): # Import string not included in generated class string import_string =\ + "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + \ "from dash.development.base_component import Component\n\n\n" # Class string generated from generate_class_string From 0fb3ed9dd0dad24988c291093a3d5736dec774e2 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Sun, 24 Jun 2018 20:31:01 -0400 Subject: [PATCH 07/14] Small fix to correctly set class _namespace attribute. --- dash/development/component_loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 51972588ee..e8ba56866e 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -55,7 +55,7 @@ def load_components(metadata_path, return components -def generate_classes(metadata_path='lib/metadata.json', output_dir='lib'): +def generate_classes(namespace, metadata_path='lib/metadata.json'): """Load React component metadata into a format Dash can parse, then create python class files. @@ -86,5 +86,5 @@ def generate_classes(metadata_path='lib/metadata.json', output_dir='lib'): name, componentData['props'], componentData['description'], - output_dir + namespace ) From c05890968898cb7f7aec7800743023fc228be631 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Sun, 24 Jun 2018 22:09:37 -0400 Subject: [PATCH 08/14] Generate an _imports_.py file so py classes import normally. --- dash/development/component_loader.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index e8ba56866e..74d2e557d4 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -1,5 +1,6 @@ import collections import json +import os from .base_component import generate_class from .base_component import generate_class_file @@ -62,15 +63,20 @@ def generate_classes(namespace, metadata_path='lib/metadata.json'): Usage: generate_classes() Keyword arguments: + namespace -- name of the generated python package (also output dir) + metadata_path -- a path to a JSON file created by [`react-docgen`](https://github.com/reactjs/react-docgen). - output_dir -- the directory to put component classes in - Returns: """ data = _get_metadata(metadata_path) + imports_path = os.path.join(namespace, '_imports_.py') + + # Make sure the file doesn't exist, as we use append write + if os.path.exists(imports_path): + os.remove(imports_path) # Iterate over each property name (which is a path to the component) for componentPath in data: @@ -88,3 +94,16 @@ def generate_classes(namespace, metadata_path='lib/metadata.json'): componentData['description'], namespace ) + + # Add an import statement for this component + with open(imports_path, 'a') as f: + f.write('from .{0:s} import {0:s}\n'.format(name)) + + # Add the __all__ value so we can import * from _imports_ + all_imports = [p.split('/').pop().split('.')[0] for p in data] + with open(imports_path, 'a') as f: + array_string = '[\n' + for a in all_imports: + array_string += ' "{:s}",\n'.format(a) + array_string += ']\n' + f.write('\n\n__all__ = {:s}'.format(array_string)) From 84c52140b8e9f2dd7ce088ff6e78d91e9c605fe9 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Sun, 24 Jun 2018 22:14:34 -0400 Subject: [PATCH 09/14] Fix order of arguments in test case for generate_classes --- tests/development/test_component_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/development/test_component_loader.py b/tests/development/test_component_loader.py index cff677b603..6e2de8f601 100644 --- a/tests/development/test_component_loader.py +++ b/tests/development/test_component_loader.py @@ -185,7 +185,7 @@ def test_loadcomponents(self): 'default_namespace' ) - generate_classes(METADATA_PATH, 'default_namespace') + generate_classes('default_namespace', METADATA_PATH) from default_namespace.MyComponent import MyComponent \ as MyComponent_buildtime from default_namespace.A import A as A_buildtime From a3a6a46dc2aeed25fc1fe66849aba7b81807aab1 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Tue, 26 Jun 2018 22:20:25 -0400 Subject: [PATCH 10/14] Real arguments instead of **kwargs --- dash/development/base_component.py | 53 +++++++++++++++++++----- tests/development/metadata_test.py | 15 ++++--- tests/development/test_base_component.py | 18 +++++--- 3 files changed, 66 insertions(+), 20 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index bda33b787c..299febe8c5 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -19,6 +19,29 @@ def _check_if_has_indexable_children(item): raise KeyError +def _explicitize_args(func): + # Python 2 + if hasattr(func, 'func_code'): + varnames = func.func_code.co_varnames + # Python 3 + else: + varnames = func.__code__.co_varnames + + def wrapper(*args, **kwargs): + if '_explicit_params' in kwargs.keys(): + raise Exception('Variable _explicit_params should not be set.') + kwargs['_explicit_params'] = \ + list( + set( + list(varnames[:len(args)]) + [k for k, _ in kwargs.items()] + ) + ) + if 'self' in kwargs['_explicit_params']: + kwargs['_explicit_params'].remove('self') + return func(*args, **kwargs) + return wrapper + + class Component(collections.MutableMapping): def __init__(self, **kwargs): # pylint: disable=super-init-not-called @@ -238,7 +261,8 @@ def generate_class_string(typename, props, description, namespace): # not all component authors will supply those. c = '''class {typename}(Component): """{docstring}""" - def __init__(self, {default_argtext}): + @_explicitize_args + def __init__(self, {default_argtext}, **kwargs): self._prop_names = {list_of_valid_keys} self._type = '{typename}' self._namespace = '{namespace}' @@ -249,11 +273,15 @@ def __init__(self, {default_argtext}): self.available_wildcard_properties =\ {list_of_valid_wildcard_attr_prefixes} + _explicit_params = kwargs.pop('_explicit_params') + _locals = locals() + _locals.update(kwargs) # For wildcard attrs + args = {{k: _locals[k] for k in _explicit_params if k != 'children'}} + for k in {required_args}: - if k not in kwargs: + if k not in args: raise TypeError( 'Required argument `' + k + '` was not specified.') - super({typename}, self).__init__({argtext}) def __repr__(self): @@ -294,16 +322,20 @@ def __repr__(self): # pylint: disable=unused-variable events = '[' + ', '.join(parse_events(props)) + ']' + prop_keys = list(props.keys()) if 'children' in props: - default_argtext = 'children=None, **kwargs' + prop_keys.remove('children') + default_argtext = "children=None, " # pylint: disable=unused-variable - argtext = 'children=children, **kwargs' + argtext = 'children=children, **args' else: - default_argtext = '**kwargs' - argtext = '**kwargs' + default_argtext = "" + argtext = '**args' + default_argtext += ", ".join( + ['{:s}=None'.format(p) for p in prop_keys if not p.endswith("-*")] + ) required_args = required_props(props) - return c.format(**locals()) @@ -325,7 +357,8 @@ def generate_class_file(typename, props, description, namespace): """ import_string =\ "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + \ - "from dash.development.base_component import Component\n\n\n" + "from dash.development.base_component import " + \ + "Component, _explicitize_args\n\n\n" class_string = generate_class_string( typename, props, @@ -357,7 +390,7 @@ def generate_class(typename, props, description, namespace): """ string = generate_class_string(typename, props, description, namespace) - scope = {'Component': Component} + scope = {'Component': Component, '_explicitize_args': _explicitize_args} # pylint: disable=exec-used exec(string, scope) result = scope[typename] diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py index b25364e559..970f7099fd 100644 --- a/tests/development/metadata_test.py +++ b/tests/development/metadata_test.py @@ -1,6 +1,6 @@ # AUTO GENERATED FILE - DO NOT EDIT -from dash.development.base_component import Component +from dash.development.base_component import Component, _explicitize_args class Table(Component): @@ -37,7 +37,8 @@ class Table(Component): - id (string; optional) Available events: 'restyle', 'relayout', 'click'""" - def __init__(self, children=None, **kwargs): + @_explicitize_args + def __init__(self, children=None, optionalArray=None, optionalBool=None, optionalFunc=None, optionalNumber=None, optionalObject=None, optionalString=None, optionalSymbol=None, optionalNode=None, optionalElement=None, optionalMessage=None, optionalEnum=None, optionalUnion=None, optionalArrayOf=None, optionalObjectOf=None, optionalObjectWithShapeAndNestedDescription=None, optionalAny=None, customProp=None, customArrayProp=None, id=None, dashEvents=None, **kwargs): self._prop_names = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', 'id'] self._type = 'Table' self._namespace = 'TableComponents' @@ -46,12 +47,16 @@ def __init__(self, children=None, **kwargs): self.available_properties = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', 'id'] self.available_wildcard_properties = ['data-', 'aria-'] + _explicit_params = kwargs.pop('_explicit_params') + _locals = locals() + _locals.update(kwargs) # For wildcard attrs + args = {k: _locals[k] for k in _explicit_params if k != 'children'} + for k in []: - if k not in kwargs: + if k not in args: raise TypeError( 'Required argument `' + k + '` was not specified.') - - super(Table, self).__init__(children=children, **kwargs) + super(Table, self).__init__(children=children, **args) def __repr__(self): if(any(getattr(self, c, None) is not None diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index cab2b0a36e..15b8d7898c 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -12,6 +12,7 @@ generate_class_string, generate_class_file, Component, + _explicitize_args, js_to_py_type, create_docstring, parse_events @@ -507,7 +508,8 @@ def setUp(self): # Import string not included in generated class string import_string =\ "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + \ - "from dash.development.base_component import Component\n\n\n" + "from dash.development.base_component import" + \ + " Component, _explicitize_args\n\n\n" # Class string generated from generate_class_string self.component_class_string = import_string + generate_class_string( @@ -684,16 +686,22 @@ def test_events(self): ['restyle', 'relayout', 'click'] ) + # This one is kind of pointless now def test_call_signature(self): + __init__func = self.ComponentClass.__init__ # TODO: Will break in Python 3 # http://stackoverflow.com/questions/2677185/ self.assertEqual( - inspect.getargspec(self.ComponentClass.__init__).args, - ['self', 'children'] + inspect.getargspec(__init__func).args, + [] ) self.assertEqual( - inspect.getargspec(self.ComponentClass.__init__).defaults, - (None, ) + inspect.getargspec(__init__func).varargs, + 'args' + ) + self.assertEqual( + inspect.getargspec(__init__func).keywords, + 'kwargs' ) def test_required_props(self): From 9140f27e26c1ec2318261481600a134daa7e6c2c Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Thu, 28 Jun 2018 08:39:23 -0400 Subject: [PATCH 11/14] In Python 3, spoof the __init__ function signature. --- dash/development/base_component.py | 23 +++++++++++----- dash/version.py | 2 +- tests/development/metadata_test.py | 4 +-- tests/development/test_base_component.py | 35 ++++++++++++++++++++++-- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 299febe8c5..f0f48de7c3 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -1,6 +1,7 @@ import collections import copy import os +import inspect def is_number(s): @@ -28,17 +29,25 @@ def _explicitize_args(func): varnames = func.__code__.co_varnames def wrapper(*args, **kwargs): - if '_explicit_params' in kwargs.keys(): - raise Exception('Variable _explicit_params should not be set.') - kwargs['_explicit_params'] = \ + 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_params']: - kwargs['_explicit_params'].remove('self') + 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 @@ -273,10 +282,10 @@ def __init__(self, {default_argtext}, **kwargs): self.available_wildcard_properties =\ {list_of_valid_wildcard_attr_prefixes} - _explicit_params = kwargs.pop('_explicit_params') + _explicit_args = kwargs.pop('_explicit_args') _locals = locals() _locals.update(kwargs) # For wildcard attrs - args = {{k: _locals[k] for k in _explicit_params if k != 'children'}} + args = {{k: _locals[k] for k in _explicit_args if k != 'children'}} for k in {required_args}: if k not in args: diff --git a/dash/version.py b/dash/version.py index 8c306aa668..81edede8b4 100644 --- a/dash/version.py +++ b/dash/version.py @@ -1 +1 @@ -__version__ = '0.21.1' +__version__ = '0.22.0' diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py index 970f7099fd..40f38c0dc6 100644 --- a/tests/development/metadata_test.py +++ b/tests/development/metadata_test.py @@ -47,10 +47,10 @@ def __init__(self, children=None, optionalArray=None, optionalBool=None, optiona self.available_properties = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', 'id'] self.available_wildcard_properties = ['data-', 'aria-'] - _explicit_params = kwargs.pop('_explicit_params') + _explicit_args = kwargs.pop('_explicit_args') _locals = locals() _locals.update(kwargs) # For wildcard attrs - args = {k: _locals[k] for k in _explicit_params if k != 'children'} + args = {k: _locals[k] for k in _explicit_args if k != 'children'} for k in []: if k not in args: diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 15b8d7898c..71bf32e050 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -693,16 +693,47 @@ def test_call_signature(self): # http://stackoverflow.com/questions/2677185/ self.assertEqual( inspect.getargspec(__init__func).args, - [] + ['self', + 'children', + 'optionalArray', + 'optionalBool', + 'optionalFunc', + 'optionalNumber', + 'optionalObject', + 'optionalString', + 'optionalSymbol', + 'optionalNode', + 'optionalElement', + 'optionalMessage', + 'optionalEnum', + 'optionalUnion', + 'optionalArrayOf', + 'optionalObjectOf', + 'optionalObjectWithShapeAndNestedDescription', + 'optionalAny', + 'customProp', + 'customArrayProp', + 'id', + 'dashEvents'] if hasattr(inspect, 'signature') else [] + + ) self.assertEqual( inspect.getargspec(__init__func).varargs, - 'args' + None if hasattr(inspect, 'signature') else 'args' ) self.assertEqual( inspect.getargspec(__init__func).keywords, 'kwargs' ) + self.assertEqual( + inspect.getargspec(__init__func).defaults, + (None, None, None, None, None, + None, None, None, None, None, + None, None, None, None, None, + None, None, None, + None, None, None) if hasattr(inspect, 'signature') else None + ) def test_required_props(self): with self.assertRaises(Exception): From 3c6e4cfb4f09f2c3af6c0e2c7fa7996d5767fc1d Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Sat, 14 Jul 2018 21:07:53 -0400 Subject: [PATCH 12/14] More semantic default arugments objects --- dash/development/base_component.py | 25 +++++++++++++++++++++++- tests/development/metadata_test.py | 2 +- tests/development/test_base_component.py | 16 ++++++--------- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index f0f48de7c3..457be6b804 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -52,6 +52,24 @@ def wrapper(*args, **kwargs): class Component(collections.MutableMapping): + class _UNDEFINED(object): + def __repr__(self): + return 'undefined' + + def __str__(self): + return 'undefined' + + UNDEFINED = _UNDEFINED() + + class _REQUIRED(object): + def __repr__(self): + return 'required' + + def __str__(self): + return 'required' + + REQUIRED = _REQUIRED() + def __init__(self, **kwargs): # pylint: disable=super-init-not-called for k, v in list(kwargs.items()): @@ -341,7 +359,12 @@ def __repr__(self): default_argtext = "" argtext = '**args' default_argtext += ", ".join( - ['{:s}=None'.format(p) for p in prop_keys if not p.endswith("-*")] + [('{: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 ['dashEvents', 'fireEvent', 'setProps']] ) required_args = required_props(props) diff --git a/tests/development/metadata_test.py b/tests/development/metadata_test.py index 40f38c0dc6..de56679fc9 100644 --- a/tests/development/metadata_test.py +++ b/tests/development/metadata_test.py @@ -38,7 +38,7 @@ class Table(Component): Available events: 'restyle', 'relayout', 'click'""" @_explicitize_args - def __init__(self, children=None, optionalArray=None, optionalBool=None, optionalFunc=None, optionalNumber=None, optionalObject=None, optionalString=None, optionalSymbol=None, optionalNode=None, optionalElement=None, optionalMessage=None, optionalEnum=None, optionalUnion=None, optionalArrayOf=None, optionalObjectOf=None, optionalObjectWithShapeAndNestedDescription=None, optionalAny=None, customProp=None, customArrayProp=None, id=None, dashEvents=None, **kwargs): + 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-*', 'id'] self._type = 'Table' self._namespace = 'TableComponents' diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index 71bf32e050..625e5329cb 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -713,8 +713,7 @@ def test_call_signature(self): 'optionalAny', 'customProp', 'customArrayProp', - 'id', - 'dashEvents'] if hasattr(inspect, 'signature') else [] + 'id'] if hasattr(inspect, 'signature') else [] ) @@ -726,14 +725,11 @@ def test_call_signature(self): inspect.getargspec(__init__func).keywords, 'kwargs' ) - self.assertEqual( - inspect.getargspec(__init__func).defaults, - (None, None, None, None, None, - None, None, None, None, None, - None, None, None, None, None, - None, None, None, - None, None, None) if hasattr(inspect, 'signature') else None - ) + if hasattr(inspect, 'signature'): + self.assertEqual( + [str(x) for x in inspect.getargspec(__init__func).defaults], + ['None'] + ['undefined'] * 19 + ) def test_required_props(self): with self.assertRaises(Exception): From ac52a5943e9d5996541600186a11520a46ffd565 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Wed, 1 Aug 2018 16:22:18 -0400 Subject: [PATCH 13/14] Update changelog and version. --- CHANGELOG.md | 4 ++++ dash/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3455701ca7..0360f4c69c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.23.0 - 2018-08-01 +## Added +- Dash components are now generated at build-time and then imported rather than generated when a module is imported. This should reduce the time it takes to import Dash component libraries, and makes Dash compatible with IDEs. + ## 0.21.1 - 2018-04-10 ## Added - `aria-*` and `data-*` attributes are now supported in all dash html components. (#40) diff --git a/dash/version.py b/dash/version.py index 81edede8b4..08a9dbff61 100644 --- a/dash/version.py +++ b/dash/version.py @@ -1 +1 @@ -__version__ = '0.22.0' +__version__ = '0.23.0' From 5858699d134960a14075b33ef3e8b0c069bd7f89 Mon Sep 17 00:00:00 2001 From: Ryan Marren Date: Wed, 1 Aug 2018 16:26:10 -0400 Subject: [PATCH 14/14] Remove extra line in version.py --- dash/version.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dash/version.py b/dash/version.py index b136463cfc..08a9dbff61 100644 --- a/dash/version.py +++ b/dash/version.py @@ -1,2 +1 @@ __version__ = '0.23.0' -