From d5421184271d764acea8a9b4265b842138bd21f3 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 6 Nov 2018 17:13:00 -0500 Subject: [PATCH 01/25] Add extract-meta.js. --- dash/extract-meta.js | 93 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 dash/extract-meta.js diff --git a/dash/extract-meta.js b/dash/extract-meta.js new file mode 100644 index 0000000000..8394bcfeed --- /dev/null +++ b/dash/extract-meta.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const reactDocs = require('react-docgen'); + +const componentPaths = process.argv.slice(2); +if (!componentPaths.length) { + help(); + process.exit(1); +} + +const metadata = Object.create(null); +componentPaths.forEach(componentPath => + collectMetadataRecursively(componentPath) +); +writeOut(metadata); + +function help() { + console.error('usage: '); + console.error( + 'extract-meta path/to/component(s) ' + + ' [path/to/more/component(s), ...] > metadata.json' + ); +} + +function writeError(msg, filePath) { + if (filePath) { + process.stderr.write(`Error with path ${filePath}`); + } + + process.stderr.write(msg + '\n'); + if (msg instanceof Error) { + process.stderr.write(msg.stack + '\n'); + } +} + +function checkWarn(name, value) { + if (value.length < 1) { + process.stderr.write(`\nDescription for ${name} is missing!\n`) + } +} + +function docstringWarning(doc) { + checkWarn(doc.displayName, doc.description); + + Object.entries(doc.props).forEach( + ([name, p]) => checkWarn(`${doc.displayName}.${name}`, p.description) + ); +} + + +function parseFile(filepath) { + const urlpath = filepath.split(path.sep).join('/'); + let src; + + if (!['.jsx', '.js'].includes(path.extname(filepath))) { + return; + } + + try { + src = fs.readFileSync(filepath); + const doc = metadata[urlpath] = reactDocs.parse(src); + docstringWarning(doc); + } catch (error) { + writeError(error, filepath); + } +} + +function collectMetadataRecursively(componentPath) { + if (fs.lstatSync(componentPath).isDirectory()) { + let dirs; + try { + dirs = fs.readdirSync(componentPath); + } catch (error) { + writeError(error, componentPath); + } + dirs.forEach(filename => { + const filepath = path.join(componentPath, filename); + if (fs.lstatSync(filepath).isDirectory()) { + collectMetadataRecursively(filepath); + } else { + parseFile(filepath); + } + }); + } else { + parseFile(componentPath); + } +} + +function writeOut(result) { + console.log(JSON.stringify(result, '\t', 2)); +} From 9863b9dfb81526133a696c1c1fe2cb1115ecb53c Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 6 Nov 2018 17:15:28 -0500 Subject: [PATCH 02/25] Combine extract-meta & component generation, wrap as cli. --- dash/development/component_generator.py | 62 +++++++++++++++++++++++++ setup.py | 5 ++ 2 files changed, 67 insertions(+) create mode 100644 dash/development/component_generator.py diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py new file mode 100644 index 0000000000..76280ae9ae --- /dev/null +++ b/dash/development/component_generator.py @@ -0,0 +1,62 @@ +from __future__ import print_function + +import json +import sys +import subprocess +import shlex +import os + +from .base_component import generate_class_file + + +def generate_components(component_src, output_dir): + is_windows = sys.platform == 'win32' + + extract_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), + '..', + 'extract-meta.js' + )) + + os.environ['NODE_PATH'] = 'node_modules' + cmd = shlex.split('node {} {}'.format(extract_path, component_src), + posix=not is_windows) + + namespace = os.path.basename(output_dir) + + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=not is_windows) + out, err = proc.communicate() + status = proc.poll() + + if err: + print(err.decode()) + + if not out: + print( + 'Error generating {} metadata in {} (status={})'.format( + namespace, output_dir, status), + file=sys.stderr) + sys.exit(-1) + metadata = json.loads(out.decode()) + for component_path, component_data in metadata.items(): + name = component_path.split('/').pop().split('.')[0] + generate_class_file( + name, + component_data['props'], + component_data['description'], + namespace + ) + print('Generated {}/{}.py'.format(namespace, name)) + + +def cli(): + # pylint: disable=unbalanced-tuple-unpacking + src, out = sys.argv[1:] + generate_components(src, out) + + +if __name__ == '__main__': + cli() diff --git a/setup.py b/setup.py index bee83d7eb7..9f22c8a4d1 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,11 @@ 'plotly', 'dash_renderer', ], + entry_points={ + 'console_scripts': [ + 'dash-generate-components = dash.development.component_generator:cli' + ] + }, url='https://plot.ly/dash', classifiers=[ 'Development Status :: 5 - Production/Stable', From 0c0d755798129a0a114136309f81c1168eee3f83 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 6 Nov 2018 17:24:54 -0500 Subject: [PATCH 03/25] Fix setup line len --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9f22c8a4d1..2182122ced 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,8 @@ ], entry_points={ 'console_scripts': [ - 'dash-generate-components = dash.development.component_generator:cli' + 'dash-generate-components =' + ' dash.development.component_generator:cli' ] }, url='https://plot.ly/dash', From 1821b287a8c19a3f43011c2de157b37d20d6079a Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 6 Nov 2018 19:14:40 -0500 Subject: [PATCH 04/25] Add arguments validation. --- dash/development/component_generator.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 76280ae9ae..4a8e3e9e6a 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -53,6 +53,14 @@ def generate_components(component_src, output_dir): def cli(): + if len(sys.argv) != 3: + print( + 'Invalid number of arguments' + ' expected 2 but got {}\n\n' + 'Arguments: src output_directory'.format(len(sys.argv) - 1), + file=sys.stderr + ) + sys.exit(-1) # pylint: disable=unbalanced-tuple-unpacking src, out = sys.argv[1:] generate_components(src, out) From fde8d6c9cd40925f0c646df0d8a85c69f049fffe Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 6 Nov 2018 19:19:18 -0500 Subject: [PATCH 05/25] Add extract-meta to manifest. --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index af967cd137..caa2d28999 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include README.md include LICENSE include dash/favicon.ico +include dash/extract-meta.js From 087a63f8bfbc543b0fc0d872ee37d55ae211c2f9 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 6 Nov 2018 20:12:25 -0500 Subject: [PATCH 06/25] Print extract-meta errors to stderr --- dash/development/component_generator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 4a8e3e9e6a..465f50fdd0 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -32,14 +32,14 @@ def generate_components(component_src, output_dir): status = proc.poll() if err: - print(err.decode()) + print(err.decode(), file=sys.stderr) if not out: print( 'Error generating {} metadata in {} (status={})'.format( namespace, output_dir, status), file=sys.stderr) - sys.exit(-1) + sys.exit(1) metadata = json.loads(out.decode()) for component_path, component_data in metadata.items(): name = component_path.split('/').pop().split('.')[0] @@ -60,7 +60,7 @@ def cli(): 'Arguments: src output_directory'.format(len(sys.argv) - 1), file=sys.stderr ) - sys.exit(-1) + sys.exit(1) # pylint: disable=unbalanced-tuple-unpacking src, out = sys.argv[1:] generate_components(src, out) From 1e5adce2132c0548daec2a546b16fb5a35ea8af7 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 6 Nov 2018 20:18:44 -0500 Subject: [PATCH 07/25] Dump metadata.json --- dash/development/component_generator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 465f50fdd0..3c13c11910 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -40,9 +40,11 @@ def generate_components(component_src, output_dir): namespace, output_dir, status), file=sys.stderr) sys.exit(1) + metadata = json.loads(out.decode()) + for component_path, component_data in metadata.items(): - name = component_path.split('/').pop().split('.')[0] + name = component_path.split('/')[-1].split('.')[0] generate_class_file( name, component_data['props'], @@ -51,6 +53,9 @@ def generate_components(component_src, output_dir): ) print('Generated {}/{}.py'.format(namespace, name)) + with open(os.path.join(output_dir, 'metadata.json'), 'w') as f: + json.dump(metadata, f) + def cli(): if len(sys.argv) != 3: From 726623db724abeb907ec1e2eef6f73b8d9fc8f2c Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 6 Nov 2018 22:01:04 -0500 Subject: [PATCH 08/25] Add _imports_.py generation. --- dash/development/component_generator.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 3c13c11910..e4d56c33b9 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -5,10 +5,12 @@ import subprocess import shlex import os +import textwrap from .base_component import generate_class_file +# pylint: disable=too-many-locals def generate_components(component_src, output_dir): is_windows = sys.platform == 'win32' @@ -43,8 +45,11 @@ def generate_components(component_src, output_dir): metadata = json.loads(out.decode()) + components = [] + for component_path, component_data in metadata.items(): name = component_path.split('/')[-1].split('.')[0] + components.append(name) generate_class_file( name, component_data['props'], @@ -56,6 +61,20 @@ def generate_components(component_src, output_dir): with open(os.path.join(output_dir, 'metadata.json'), 'w') as f: json.dump(metadata, f) + with open(os.path.join(output_dir, '_imports_.py'), 'w') as f: + f.write(textwrap.dedent( + ''' + {} + + __all__ = [ + {} + ] + '''.format( + '\n'.join('from {0} import {0}'.format(x) for x in components), + ',\n'.join(' "{}"'.format(x) for x in components) + ) + )) + def cli(): if len(sys.argv) != 3: From 1bbd7fbe7fd00e080976a19b76e2d3262fba95c5 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 7 Nov 2018 16:33:42 -0500 Subject: [PATCH 09/25] Strip first newline from `_imports_.py`. --- dash/development/component_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index e4d56c33b9..53f77e3638 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -73,7 +73,7 @@ def generate_components(component_src, output_dir): '\n'.join('from {0} import {0}'.format(x) for x in components), ',\n'.join(' "{}"'.format(x) for x in components) ) - )) + ).lstrip()) def cli(): From 98811a141b47bdc2b192b840ca9b8d78c8e711e3 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 7 Nov 2018 18:18:51 -0500 Subject: [PATCH 10/25] Change output_dir to project_shortname. --- dash/development/component_generator.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 53f77e3638..a3c6027262 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -11,7 +11,7 @@ # pylint: disable=too-many-locals -def generate_components(component_src, output_dir): +def generate_components(component_src, project_shortname): is_windows = sys.platform == 'win32' extract_path = os.path.abspath(os.path.join( @@ -24,8 +24,6 @@ def generate_components(component_src, output_dir): cmd = shlex.split('node {} {}'.format(extract_path, component_src), posix=not is_windows) - namespace = os.path.basename(output_dir) - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -38,8 +36,8 @@ def generate_components(component_src, output_dir): if not out: print( - 'Error generating {} metadata in {} (status={})'.format( - namespace, output_dir, status), + 'Error generating metadata in {} (status={})'.format( + project_shortname, status), file=sys.stderr) sys.exit(1) @@ -54,14 +52,14 @@ def generate_components(component_src, output_dir): name, component_data['props'], component_data['description'], - namespace + project_shortname ) - print('Generated {}/{}.py'.format(namespace, name)) + print('Generated {}/{}.py'.format(project_shortname, name)) - with open(os.path.join(output_dir, 'metadata.json'), 'w') as f: + with open(os.path.join(project_shortname, 'metadata.json'), 'w') as f: json.dump(metadata, f) - with open(os.path.join(output_dir, '_imports_.py'), 'w') as f: + with open(os.path.join(project_shortname, '_imports_.py'), 'w') as f: f.write(textwrap.dedent( ''' {} @@ -81,13 +79,13 @@ def cli(): print( 'Invalid number of arguments' ' expected 2 but got {}\n\n' - 'Arguments: src output_directory'.format(len(sys.argv) - 1), + 'Arguments: src project_shortname'.format(len(sys.argv) - 1), file=sys.stderr ) sys.exit(1) # pylint: disable=unbalanced-tuple-unpacking - src, out = sys.argv[1:] - generate_components(src, out) + src, project_shortname = sys.argv[1:] + generate_components(src, project_shortname) if __name__ == '__main__': From 757327ac8897efcd8fbb22f6da2c34ea2ee33b9b Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Mon, 12 Nov 2018 17:33:03 -0500 Subject: [PATCH 11/25] Fix shell is_windows. --- dash/development/component_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index a3c6027262..6e292269e3 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -27,7 +27,7 @@ def generate_components(component_src, project_shortname): proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=not is_windows) + shell=is_windows) out, err = proc.communicate() status = proc.poll() From b5f619947bb4a1006529c53b6f4094cd86f497d6 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Mon, 12 Nov 2018 19:17:41 -0500 Subject: [PATCH 12/25] Relative import the components --- dash/development/component_generator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 6e292269e3..79cec46df1 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -68,7 +68,8 @@ def generate_components(component_src, project_shortname): {} ] '''.format( - '\n'.join('from {0} import {0}'.format(x) for x in components), + '\n'.join( + 'from .{0} import {0}'.format(x) for x in components), ',\n'.join(' "{}"'.format(x) for x in components) ) ).lstrip()) From b8339d1f6f0562db76f04458c1fc39c680613f25 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 13 Nov 2018 13:31:40 -0500 Subject: [PATCH 13/25] Rename name->component_name --- dash/development/component_generator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 79cec46df1..ad6dd09435 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -46,15 +46,15 @@ def generate_components(component_src, project_shortname): components = [] for component_path, component_data in metadata.items(): - name = component_path.split('/')[-1].split('.')[0] - components.append(name) + component_name = component_path.split('/')[-1].split('.')[0] + components.append(component_name) generate_class_file( - name, + component_name, component_data['props'], component_data['description'], project_shortname ) - print('Generated {}/{}.py'.format(project_shortname, name)) + print('Generated {}/{}.py'.format(project_shortname, component_name)) with open(os.path.join(project_shortname, 'metadata.json'), 'w') as f: json.dump(metadata, f) From 68087cf811fe6beb1586906817fe30f60fe1b1d5 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 13 Nov 2018 13:33:48 -0500 Subject: [PATCH 14/25] Move generated print to generate_class_file. --- dash/development/base_component.py | 2 ++ dash/development/component_generator.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 789253a3f2..bff5864fc2 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -465,6 +465,8 @@ def generate_class_file(typename, props, description, namespace): with open(file_path, 'w') as f: f.write(import_string) f.write(class_string) + + print('Generated {}'.format(file_name)) # pylint: disable=unused-argument diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index ad6dd09435..948a826eb9 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -54,7 +54,6 @@ def generate_components(component_src, project_shortname): component_data['description'], project_shortname ) - print('Generated {}/{}.py'.format(project_shortname, component_name)) with open(os.path.join(project_shortname, 'metadata.json'), 'w') as f: json.dump(metadata, f) From b3070dcd535c617b50b684ac57f632c163c16e6c Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 13 Nov 2018 13:39:14 -0500 Subject: [PATCH 15/25] :camel: generate_imports --- dash/development/base_component.py | 2 +- dash/development/component_generator.py | 17 ++----------- dash/development/component_loader.py | 34 +++++++++++++++++-------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index bff5864fc2..10db6e0f11 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -465,7 +465,7 @@ def generate_class_file(typename, props, description, namespace): with open(file_path, 'w') as f: f.write(import_string) f.write(class_string) - + print('Generated {}'.format(file_name)) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 948a826eb9..941b91eaf9 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -5,8 +5,8 @@ import subprocess import shlex import os -import textwrap +from dash.development.component_loader import generate_imports from .base_component import generate_class_file @@ -58,20 +58,7 @@ def generate_components(component_src, project_shortname): with open(os.path.join(project_shortname, 'metadata.json'), 'w') as f: json.dump(metadata, f) - with open(os.path.join(project_shortname, '_imports_.py'), 'w') as f: - f.write(textwrap.dedent( - ''' - {} - - __all__ = [ - {} - ] - '''.format( - '\n'.join( - 'from .{0} import {0}'.format(x) for x in components), - ',\n'.join(' "{}"'.format(x) for x in components) - ) - ).lstrip()) + generate_imports(project_shortname, components) def cli(): diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 2b5e70b10f..6029e1950f 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -1,6 +1,8 @@ import collections import json import os +import textwrap + from .base_component import generate_class from .base_component import generate_class_file from .base_component import ComponentRegistry @@ -16,6 +18,23 @@ def _get_metadata(metadata_path): return data +def generate_imports(project_shortname, components): + with open(os.path.join(project_shortname, '_imports_.py'), 'w') as f: + f.write(textwrap.dedent( + ''' + {} + + __all__ = [ + {} + ] + '''.format( + '\n'.join( + 'from .{0} import {0}'.format(x) for x in components), + ',\n'.join(' "{}"'.format(x) for x in components) + ) + ).lstrip()) + + def load_components(metadata_path, namespace='default_namespace'): """Load React component metadata into a format Dash can parse. @@ -81,6 +100,8 @@ def generate_classes(namespace, metadata_path='lib/metadata.json'): if os.path.exists(imports_path): os.remove(imports_path) + components = [] + # Iterate over each property name (which is a path to the component) for componentPath in data: componentData = data[componentPath] @@ -97,16 +118,7 @@ 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)) + components.append(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)) + generate_imports(namespace, components) From 474f99539d46562f246d9f8c351cded1c23b5a40 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 13 Nov 2018 13:52:18 -0500 Subject: [PATCH 16/25] Disable superflous-parens pylint. --- .pylintrc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index fcf0968f10..a9688e4e7a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -58,7 +58,9 @@ disable=fixme, missing-docstring, invalid-name, too-many-lines, - old-style-class + old-style-class, + superfluous-parens + # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where From 414ac623c34bea634800ba1bde69f0828ea9d5c9 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 13 Nov 2018 14:15:53 -0500 Subject: [PATCH 17/25] relative import. --- dash/development/component_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 941b91eaf9..008af05ea5 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -6,7 +6,7 @@ import shlex import os -from dash.development.component_loader import generate_imports +from .component_loader import generate_imports from .base_component import generate_class_file From 25a8e94634bab006c8f7fd177fbde78a4913e86e Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 13 Nov 2018 19:27:42 -0500 Subject: [PATCH 18/25] :camel: generate classes files loop. --- dash/development/component_generator.py | 18 ++++-------- dash/development/component_loader.py | 37 ++++++++++++------------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 008af05ea5..c7072d0690 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -6,7 +6,7 @@ import shlex import os -from .component_loader import generate_imports +from .component_loader import generate_imports, generate_classes_files from .base_component import generate_class_file @@ -43,17 +43,11 @@ def generate_components(component_src, project_shortname): metadata = json.loads(out.decode()) - components = [] - - for component_path, component_data in metadata.items(): - component_name = component_path.split('/')[-1].split('.')[0] - components.append(component_name) - generate_class_file( - component_name, - component_data['props'], - component_data['description'], - project_shortname - ) + components = generate_classes_files( + project_shortname, + metadata, + generate_class_file + ) with open(os.path.join(project_shortname, 'metadata.json'), 'w') as f: json.dump(metadata, f) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 6029e1950f..a055230271 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -78,6 +78,23 @@ def load_components(metadata_path, return components +def generate_classes_files(project_shortname, metadata, *component_generators): + components = [] + for component_path, component_data in metadata.items(): + component_name = component_path.split('/')[-1].split('.')[0] + components.append(component_name) + + for generator in component_generators: + generator( + component_name, + component_data['props'], + component_data['description'], + project_shortname + ) + + return components + + def generate_classes(namespace, metadata_path='lib/metadata.json'): """Load React component metadata into a format Dash can parse, then create python class files. @@ -100,25 +117,7 @@ def generate_classes(namespace, metadata_path='lib/metadata.json'): if os.path.exists(imports_path): os.remove(imports_path) - components = [] - - # 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 - ) - components.append(name) + components = generate_classes_files(namespace, data, generate_class_file) # Add the __all__ value so we can import * from _imports_ generate_imports(namespace, components) From c642b84f5e0fa3cc44296d1e2797350dc1c87648 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 13 Nov 2018 19:38:48 -0500 Subject: [PATCH 19/25] :feet: Move generate imports/files methods to base_components. --- dash/development/base_component.py | 36 ++++++++++++++++++++++++ dash/development/component_generator.py | 3 +- dash/development/component_loader.py | 37 ++----------------------- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 10db6e0f11..af589f45a7 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -4,6 +4,8 @@ import inspect import abc import sys +import textwrap + import six from ._all_keywords import kwlist @@ -884,3 +886,37 @@ def js_to_py_type(type_object, is_flow_type=False, indent_num=0): # All other types return js_to_py_types[js_type_name]() return '' + + +def generate_imports(project_shortname, components): + with open(os.path.join(project_shortname, '_imports_.py'), 'w') as f: + f.write(textwrap.dedent( + ''' + {} + + __all__ = [ + {} + ] + '''.format( + '\n'.join( + 'from .{0} import {0}'.format(x) for x in components), + ',\n'.join(' "{}"'.format(x) for x in components) + ) + ).lstrip()) + + +def generate_classes_files(project_shortname, metadata, *component_generators): + components = [] + for component_path, component_data in metadata.items(): + component_name = component_path.split('/')[-1].split('.')[0] + components.append(component_name) + + for generator in component_generators: + generator( + component_name, + component_data['props'], + component_data['description'], + project_shortname + ) + + return components diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index c7072d0690..a06cd7ce40 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -6,8 +6,9 @@ import shlex import os -from .component_loader import generate_imports, generate_classes_files from .base_component import generate_class_file +from .base_component import generate_imports +from .base_component import generate_classes_files # pylint: disable=too-many-locals diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index a055230271..99a451fd29 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -1,8 +1,9 @@ import collections import json import os -import textwrap +from .base_component import generate_imports +from .base_component import generate_classes_files from .base_component import generate_class from .base_component import generate_class_file from .base_component import ComponentRegistry @@ -18,23 +19,6 @@ def _get_metadata(metadata_path): return data -def generate_imports(project_shortname, components): - with open(os.path.join(project_shortname, '_imports_.py'), 'w') as f: - f.write(textwrap.dedent( - ''' - {} - - __all__ = [ - {} - ] - '''.format( - '\n'.join( - 'from .{0} import {0}'.format(x) for x in components), - ',\n'.join(' "{}"'.format(x) for x in components) - ) - ).lstrip()) - - def load_components(metadata_path, namespace='default_namespace'): """Load React component metadata into a format Dash can parse. @@ -78,23 +62,6 @@ def load_components(metadata_path, return components -def generate_classes_files(project_shortname, metadata, *component_generators): - components = [] - for component_path, component_data in metadata.items(): - component_name = component_path.split('/')[-1].split('.')[0] - components.append(component_name) - - for generator in component_generators: - generator( - component_name, - component_data['props'], - component_data['description'], - project_shortname - ) - - return components - - def generate_classes(namespace, metadata_path='lib/metadata.json'): """Load React component metadata into a format Dash can parse, then create python class files. From b53f54a0439eceba3ee3ba7db69d763e73f91dc5 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 20 Nov 2018 16:11:08 -0500 Subject: [PATCH 20/25] Proper cli with argparse. --- dash/development/component_generator.py | 31 ++++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index a06cd7ce40..3f51b7c394 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -5,12 +5,18 @@ import subprocess import shlex import os +import argparse from .base_component import generate_class_file from .base_component import generate_imports from .base_component import generate_classes_files +class _CombinedFormatter(argparse.ArgumentDefaultsHelpFormatter, + argparse.RawDescriptionHelpFormatter): + pass + + # pylint: disable=too-many-locals def generate_components(component_src, project_shortname): is_windows = sys.platform == 'win32' @@ -57,17 +63,20 @@ def generate_components(component_src, project_shortname): def cli(): - if len(sys.argv) != 3: - print( - 'Invalid number of arguments' - ' expected 2 but got {}\n\n' - 'Arguments: src project_shortname'.format(len(sys.argv) - 1), - file=sys.stderr - ) - sys.exit(1) - # pylint: disable=unbalanced-tuple-unpacking - src, project_shortname = sys.argv[1:] - generate_components(src, project_shortname) + parser = argparse.ArgumentParser( + prog='dash-generate-components', + formatter_class=_CombinedFormatter, + description='Generate dash components by extracting the metadata ' + 'using react-docgen. Then map the metadata to python classes.' + ) + parser.add_argument('src', help='React components source directory.') + parser.add_argument( + 'project_shortname', + help='Name of the project to export the classes files.' + ) + + args = parser.parse_args() + generate_components(args.src, args.project_shortname) if __name__ == '__main__': From a3d5ae20293cd3dbdb881e26b7e6db3da2d51fcf Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 21 Nov 2018 12:10:35 -0500 Subject: [PATCH 21/25] Copy package.json, take the output filename as option. --- dash/development/component_generator.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 3f51b7c394..ad7ad62b2f 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -6,6 +6,7 @@ import shlex import os import argparse +import shutil from .base_component import generate_class_file from .base_component import generate_imports @@ -18,7 +19,8 @@ class _CombinedFormatter(argparse.ArgumentDefaultsHelpFormatter, # pylint: disable=too-many-locals -def generate_components(component_src, project_shortname): +def generate_components(component_src, project_shortname, + package_info_filename='package.json'): is_windows = sys.platform == 'win32' extract_path = os.path.abspath(os.path.join( @@ -31,6 +33,9 @@ def generate_components(component_src, project_shortname): cmd = shlex.split('node {} {}'.format(extract_path, component_src), posix=not is_windows) + shutil.copyfile('package.json', + os.path.join(project_shortname,package_info_filename)) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -74,9 +79,15 @@ def cli(): 'project_shortname', help='Name of the project to export the classes files.' ) + parser.add_argument( + '-p', '--package-info-filename', + default='package.json', + help='The filename of the copied `package.json` to `project_shortname`' + ) args = parser.parse_args() - generate_components(args.src, args.project_shortname) + generate_components(args.src, args.project_shortname, + package_info_filename=args.package_info_filename) if __name__ == '__main__': From 52a56f9f8636cecf2301c3b63c73119c7e95090d Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 21 Nov 2018 12:15:02 -0500 Subject: [PATCH 22/25] :name_badge: src -> components_sources --- dash/development/component_generator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index ad7ad62b2f..04649a691e 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -19,7 +19,7 @@ class _CombinedFormatter(argparse.ArgumentDefaultsHelpFormatter, # pylint: disable=too-many-locals -def generate_components(component_src, project_shortname, +def generate_components(components_source, project_shortname, package_info_filename='package.json'): is_windows = sys.platform == 'win32' @@ -30,11 +30,11 @@ def generate_components(component_src, project_shortname, )) os.environ['NODE_PATH'] = 'node_modules' - cmd = shlex.split('node {} {}'.format(extract_path, component_src), + cmd = shlex.split('node {} {}'.format(extract_path, components_source), posix=not is_windows) shutil.copyfile('package.json', - os.path.join(project_shortname,package_info_filename)) + os.path.join(project_shortname, package_info_filename)) proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, @@ -74,7 +74,8 @@ def cli(): description='Generate dash components by extracting the metadata ' 'using react-docgen. Then map the metadata to python classes.' ) - parser.add_argument('src', help='React components source directory.') + parser.add_argument('components_source', + help='React components source directory.') parser.add_argument( 'project_shortname', help='Name of the project to export the classes files.' @@ -86,7 +87,7 @@ def cli(): ) args = parser.parse_args() - generate_components(args.src, args.project_shortname, + generate_components(args.components_source, args.project_shortname, package_info_filename=args.package_info_filename) From 6d8e6b7f96720884aaaf0df8418e3dc0bf1b37c0 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Mon, 26 Nov 2018 16:16:28 -0500 Subject: [PATCH 23/25] Use pkg_resources to make the extract-meta.js path. --- dash/development/component_generator.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 04649a691e..6c2f9bd842 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -7,6 +7,8 @@ import os import argparse import shutil +import pkg_resources + from .base_component import generate_class_file from .base_component import generate_imports @@ -23,11 +25,7 @@ def generate_components(components_source, project_shortname, package_info_filename='package.json'): is_windows = sys.platform == 'win32' - extract_path = os.path.abspath(os.path.join( - os.path.dirname(__file__), - '..', - 'extract-meta.js' - )) + extract_path = pkg_resources.resource_filename('dash', 'extract-meta.js') os.environ['NODE_PATH'] = 'node_modules' cmd = shlex.split('node {} {}'.format(extract_path, components_source), From 4878245f2ead071bb5e78364be5c07c94e598b60 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Mon, 26 Nov 2018 16:58:23 -0500 Subject: [PATCH 24/25] :feet: Move component generation code to _py_components_generation.py --- dash/development/_py_components_generation.py | 615 ++++++++++++++++ dash/development/base_component.py | 666 +----------------- dash/development/component_generator.py | 8 +- dash/development/component_loader.py | 10 +- tests/development/test_base_component.py | 11 +- tests/development/test_component_loader.py | 2 +- tests/test_resources.py | 2 +- 7 files changed, 656 insertions(+), 658 deletions(-) create mode 100644 dash/development/_py_components_generation.py diff --git a/dash/development/_py_components_generation.py b/dash/development/_py_components_generation.py new file mode 100644 index 0000000000..ba17ccd84d --- /dev/null +++ b/dash/development/_py_components_generation.py @@ -0,0 +1,615 @@ +import collections +import copy +import os +import textwrap + +from dash.development.base_component import _explicitize_args +from ._all_keywords import kwlist +from .base_component import Component + + +# 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. + c = '''class {typename}(Component): + """{docstring}""" + @_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 if k != 'children'}} + + for k in {required_args}: + if k not in args: + raise TypeError( + 'Required argument `' + k + '` was not specified.') + 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: + prop_keys.remove('children') + default_argtext = "children=None, " + # pylint: disable=unused-variable + argtext = 'children=children, **args' + else: + 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'] + ) + + required_args = required_props(props) + return c.format(**locals()) + + +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) + + print('Generated {}'.format(file_name)) + + +def generate_imports(project_shortname, components): + with open(os.path.join(project_shortname, '_imports_.py'), 'w') as f: + f.write(textwrap.dedent( + ''' + {} + + __all__ = [ + {} + ] + '''.format( + '\n'.join( + 'from .{0} import {0}'.format(x) for x in components), + ',\n'.join(' "{}"'.format(x) for x in components) + ) + ).lstrip()) + + +def generate_classes_files(project_shortname, metadata, *component_generators): + components = [] + for component_path, component_data in metadata.items(): + component_name = component_path.split('/')[-1].split('.')[0] + components.append(component_name) + + for generator in component_generators: + generator( + component_name, + component_data['props'], + component_data['description'], + project_shortname + ) + + return components + + +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: + events = [] + + return events + + +def parse_wildcards(props): + """ + Pull out the wildcard attributes from the Component props + + Parameters + ---------- + props: dict + Dictionary with {propName: propMetadata} structure + + Returns + ------- + list + List of Dash valid wildcard prefixes + """ + list_of_valid_wildcard_attr_prefixes = [] + for wildcard_attr in ["data-*", "aria-*"]: + if wildcard_attr in props.keys(): + list_of_valid_wildcard_attr_prefixes.append(wildcard_attr[:-1]) + return list_of_valid_wildcard_attr_prefixes + + +def reorder_props(props): + """ + If "children" is in props, then move it to the + front to respect dash convention + + Parameters + ---------- + props: dict + Dictionary with {propName: propMetadata} structure + + Returns + ------- + dict + Dictionary with {propName: propMetadata} structure + """ + if 'children' in props: + props = collections.OrderedDict( + [('children', props.pop('children'),)] + + list(zip(list(props.keys()), list(props.values())))) + + return props + + +def filter_props(props): + """ + Filter props from the Component arguments to exclude: + - Those without a "type" or a "flowType" field + - Those with arg.type.name in {'func', 'symbol', 'instanceOf'} + - dashEvents as a name + + Parameters + ---------- + props: dict + Dictionary with {propName: propMetadata} structure + + Returns + ------- + dict + Filtered dictionary with {propName: propMetadata} structure + + Examples + -------- + ```python + prop_args = { + 'prop1': { + 'type': {'name': 'bool'}, + 'required': False, + 'description': 'A description', + 'flowType': {}, + 'defaultValue': {'value': 'false', 'computed': False}, + }, + 'prop2': {'description': 'A prop without a type'}, + 'prop3': { + 'type': {'name': 'func'}, + 'description': 'A function prop', + }, + } + # filtered_prop_args is now + # { + # 'prop1': { + # 'type': {'name': 'bool'}, + # 'required': False, + # 'description': 'A description', + # 'flowType': {}, + # 'defaultValue': {'value': 'false', 'computed': False}, + # }, + # } + filtered_prop_args = filter_props(prop_args) + ``` + """ + filtered_props = copy.deepcopy(props) + + for arg_name, arg in list(filtered_props.items()): + if 'type' not in arg and 'flowType' not in arg: + filtered_props.pop(arg_name) + continue + + # Filter out functions and instances -- + # these cannot be passed from Python + if 'type' in arg: # These come from PropTypes + arg_type = arg['type']['name'] + if arg_type in {'func', 'symbol', 'instanceOf'}: + filtered_props.pop(arg_name) + elif 'flowType' in arg: # These come from Flow & handled differently + arg_type_name = arg['flowType']['name'] + if arg_type_name == 'signature': + # This does the same as the PropTypes filter above, but "func" + # is under "type" if "name" is "signature" vs just in "name" + if 'type' not in arg['flowType'] \ + or arg['flowType']['type'] != 'object': + filtered_props.pop(arg_name) + else: + raise ValueError + + # dashEvents are a special oneOf property that is used for subscribing + # to events but it's never set as a property + if arg_name in ['dashEvents']: + filtered_props.pop(arg_name) + return filtered_props + + +# pylint: disable=too-many-arguments +def create_prop_docstring(prop_name, type_object, required, description, + indent_num, is_flow_type=False): + """ + Create the Dash component prop docstring + + Parameters + ---------- + prop_name: str + Name of the Dash component prop + type_object: dict + react-docgen-generated prop type dictionary + required: bool + Component is required? + description: str + Dash component description + indent_num: int + Number of indents to use for the context block + (creates 2 spaces for every indent) + is_flow_type: bool + Does the prop use Flow types? Otherwise, uses PropTypes + + Returns + ------- + str + Dash component prop docstring + """ + py_type_name = js_to_py_type( + type_object=type_object, + is_flow_type=is_flow_type, + indent_num=indent_num + 1) + + indent_spacing = ' ' * indent_num + if '\n' in py_type_name: + return '{indent_spacing}- {name} ({is_required}): {description}. ' \ + '{name} has the following type: {type}'.format( + indent_spacing=indent_spacing, + name=prop_name, + type=py_type_name, + description=description, + is_required='required' if required else 'optional') + return '{indent_spacing}- {name} ({type}' \ + '{is_required}){description}'.format( + indent_spacing=indent_spacing, + name=prop_name, + type='{}; '.format(py_type_name) if py_type_name else '', + description=( + ': {}'.format(description) if description != '' else '' + ), + is_required='required' if required else 'optional') + + +def map_js_to_py_types_prop_types(type_object): + """Mapping from the PropTypes js type object to the Python type""" + return dict( + array=lambda: 'list', + bool=lambda: 'boolean', + number=lambda: 'number', + string=lambda: 'string', + object=lambda: 'dict', + any=lambda: 'boolean | number | string | dict | list', + element=lambda: 'dash component', + node=lambda: 'a list of or a singular dash ' + 'component, string or number', + + # React's PropTypes.oneOf + enum=lambda: 'a value equal to: {}'.format( + ', '.join( + '{}'.format(str(t['value'])) + for t in type_object['value'])), + + # React's PropTypes.oneOfType + union=lambda: '{}'.format( + ' | '.join( + '{}'.format(js_to_py_type(subType)) + for subType in type_object['value'] + if js_to_py_type(subType) != '')), + + # React's PropTypes.arrayOf + arrayOf=lambda: 'list'.format( # pylint: disable=too-many-format-args + ' of {}s'.format( + js_to_py_type(type_object['value'])) + if js_to_py_type(type_object['value']) != '' + else ''), + + # React's PropTypes.objectOf + objectOf=lambda: ( + 'dict with strings as keys and values of type {}' + ).format( + js_to_py_type(type_object['value'])), + + # React's PropTypes.shape + shape=lambda: 'dict containing keys {}.\n{}'.format( + ', '.join( + "'{}'".format(t) + for t in list(type_object['value'].keys())), + 'Those keys have the following types: \n{}'.format( + '\n'.join(create_prop_docstring( + prop_name=prop_name, + type_object=prop, + required=prop['required'], + description=prop.get('description', ''), + indent_num=1) + for prop_name, prop in + list(type_object['value'].items())))), + ) + + +def map_js_to_py_types_flow_types(type_object): + """Mapping from the Flow js types to the Python type""" + return dict( + array=lambda: 'list', + boolean=lambda: 'boolean', + number=lambda: 'number', + string=lambda: 'string', + Object=lambda: 'dict', + any=lambda: 'bool | number | str | dict | list', + Element=lambda: 'dash component', + Node=lambda: 'a list of or a singular dash ' + 'component, string or number', + + # React's PropTypes.oneOfType + union=lambda: '{}'.format( + ' | '.join( + '{}'.format(js_to_py_type(subType)) + for subType in type_object['elements'] + if js_to_py_type(subType) != '')), + + # Flow's Array type + Array=lambda: 'list{}'.format( + ' of {}s'.format( + js_to_py_type(type_object['elements'][0])) + if js_to_py_type(type_object['elements'][0]) != '' + else ''), + + # React's PropTypes.shape + signature=lambda indent_num: 'dict containing keys {}.\n{}'.format( + ', '.join("'{}'".format(d['key']) + for d in type_object['signature']['properties']), + '{}Those keys have the following types: \n{}'.format( + ' ' * indent_num, + '\n'.join( + create_prop_docstring( + prop_name=prop['key'], + type_object=prop['value'], + required=prop['value']['required'], + description=prop['value'].get('description', ''), + indent_num=indent_num, + is_flow_type=True) + for prop in type_object['signature']['properties']))), + ) + + +def js_to_py_type(type_object, is_flow_type=False, indent_num=0): + """ + Convert JS types to Python types for the component definition + + Parameters + ---------- + type_object: dict + react-docgen-generated prop type dictionary + is_flow_type: bool + Does the prop use Flow types? Otherwise, uses PropTypes + indent_num: int + Number of indents to use for the docstring for the prop + + Returns + ------- + str + Python type string + """ + js_type_name = type_object['name'] + js_to_py_types = map_js_to_py_types_flow_types(type_object=type_object) \ + if is_flow_type \ + else map_js_to_py_types_prop_types(type_object=type_object) + + if 'computed' in type_object and type_object['computed'] \ + or type_object.get('type', '') == 'function': + return '' + elif js_type_name in js_to_py_types: + if js_type_name == 'signature': # This is a Flow object w/ signature + return js_to_py_types[js_type_name](indent_num) + # All other types + return js_to_py_types[js_type_name]() + return '' diff --git a/dash/development/base_component.py b/dash/development/base_component.py index af589f45a7..ce6aec0741 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -1,15 +1,10 @@ import collections -import copy -import os -import inspect import abc +import inspect import sys -import textwrap import six -from ._all_keywords import kwlist - # pylint: disable=no-init,too-few-public-methods class ComponentRegistry: @@ -68,37 +63,6 @@ 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_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 - - @six.add_metaclass(ComponentMeta) class Component(collections.MutableMapping): class _UNDEFINED(object): @@ -313,610 +277,32 @@ def __len__(self): return length -# 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. - c = '''class {typename}(Component): - """{docstring}""" - @_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 if k != 'children'}} - - for k in {required_args}: - if k not in args: - raise TypeError( - 'Required argument `' + k + '` was not specified.') - 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: - prop_keys.remove('children') - default_argtext = "children=None, " - # pylint: disable=unused-variable - argtext = 'children=children, **args' - else: - 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'] - ) - - 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) - - print('Generated {}'.format(file_name)) - - -# 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']] +def _explicitize_args(func): + # Python 2 + if hasattr(func, 'func_code'): + varnames = func.func_code.co_varnames + # Python 3 else: - events = [] - - return events - - -def parse_wildcards(props): - """ - Pull out the wildcard attributes from the Component props - - Parameters - ---------- - props: dict - Dictionary with {propName: propMetadata} structure - - Returns - ------- - list - List of Dash valid wildcard prefixes - """ - list_of_valid_wildcard_attr_prefixes = [] - for wildcard_attr in ["data-*", "aria-*"]: - if wildcard_attr in props.keys(): - list_of_valid_wildcard_attr_prefixes.append(wildcard_attr[:-1]) - return list_of_valid_wildcard_attr_prefixes - - -def reorder_props(props): - """ - If "children" is in props, then move it to the - front to respect dash convention - - Parameters - ---------- - props: dict - Dictionary with {propName: propMetadata} structure - - Returns - ------- - dict - Dictionary with {propName: propMetadata} structure - """ - if 'children' in props: - props = collections.OrderedDict( - [('children', props.pop('children'),)] + - list(zip(list(props.keys()), list(props.values())))) - - return props - - -def filter_props(props): - """ - Filter props from the Component arguments to exclude: - - Those without a "type" or a "flowType" field - - Those with arg.type.name in {'func', 'symbol', 'instanceOf'} - - dashEvents as a name - - Parameters - ---------- - props: dict - Dictionary with {propName: propMetadata} structure - - Returns - ------- - dict - Filtered dictionary with {propName: propMetadata} structure - - Examples - -------- - ```python - prop_args = { - 'prop1': { - 'type': {'name': 'bool'}, - 'required': False, - 'description': 'A description', - 'flowType': {}, - 'defaultValue': {'value': 'false', 'computed': False}, - }, - 'prop2': {'description': 'A prop without a type'}, - 'prop3': { - 'type': {'name': 'func'}, - 'description': 'A function prop', - }, - } - # filtered_prop_args is now - # { - # 'prop1': { - # 'type': {'name': 'bool'}, - # 'required': False, - # 'description': 'A description', - # 'flowType': {}, - # 'defaultValue': {'value': 'false', 'computed': False}, - # }, - # } - filtered_prop_args = filter_props(prop_args) - ``` - """ - filtered_props = copy.deepcopy(props) - - for arg_name, arg in list(filtered_props.items()): - if 'type' not in arg and 'flowType' not in arg: - filtered_props.pop(arg_name) - continue - - # Filter out functions and instances -- - # these cannot be passed from Python - if 'type' in arg: # These come from PropTypes - arg_type = arg['type']['name'] - if arg_type in {'func', 'symbol', 'instanceOf'}: - filtered_props.pop(arg_name) - elif 'flowType' in arg: # These come from Flow & handled differently - arg_type_name = arg['flowType']['name'] - if arg_type_name == 'signature': - # This does the same as the PropTypes filter above, but "func" - # is under "type" if "name" is "signature" vs just in "name" - if 'type' not in arg['flowType'] \ - or arg['flowType']['type'] != 'object': - filtered_props.pop(arg_name) - else: - raise ValueError - - # dashEvents are a special oneOf property that is used for subscribing - # to events but it's never set as a property - if arg_name in ['dashEvents']: - filtered_props.pop(arg_name) - return filtered_props - - -# pylint: disable=too-many-arguments -def create_prop_docstring(prop_name, type_object, required, description, - indent_num, is_flow_type=False): - """ - Create the Dash component prop docstring - - Parameters - ---------- - prop_name: str - Name of the Dash component prop - type_object: dict - react-docgen-generated prop type dictionary - required: bool - Component is required? - description: str - Dash component description - indent_num: int - Number of indents to use for the context block - (creates 2 spaces for every indent) - is_flow_type: bool - Does the prop use Flow types? Otherwise, uses PropTypes - - Returns - ------- - str - Dash component prop docstring - """ - py_type_name = js_to_py_type( - type_object=type_object, - is_flow_type=is_flow_type, - indent_num=indent_num + 1) - - indent_spacing = ' ' * indent_num - if '\n' in py_type_name: - return '{indent_spacing}- {name} ({is_required}): {description}. ' \ - '{name} has the following type: {type}'.format( - indent_spacing=indent_spacing, - name=prop_name, - type=py_type_name, - description=description, - is_required='required' if required else 'optional') - return '{indent_spacing}- {name} ({type}' \ - '{is_required}){description}'.format( - indent_spacing=indent_spacing, - name=prop_name, - type='{}; '.format(py_type_name) if py_type_name else '', - description=( - ': {}'.format(description) if description != '' else '' - ), - is_required='required' if required else 'optional') - - -def map_js_to_py_types_prop_types(type_object): - """Mapping from the PropTypes js type object to the Python type""" - return dict( - array=lambda: 'list', - bool=lambda: 'boolean', - number=lambda: 'number', - string=lambda: 'string', - object=lambda: 'dict', - any=lambda: 'boolean | number | string | dict | list', - element=lambda: 'dash component', - node=lambda: 'a list of or a singular dash ' - 'component, string or number', - - # React's PropTypes.oneOf - enum=lambda: 'a value equal to: {}'.format( - ', '.join( - '{}'.format(str(t['value'])) - for t in type_object['value'])), - - # React's PropTypes.oneOfType - union=lambda: '{}'.format( - ' | '.join( - '{}'.format(js_to_py_type(subType)) - for subType in type_object['value'] - if js_to_py_type(subType) != '')), - - # React's PropTypes.arrayOf - arrayOf=lambda: 'list'.format( # pylint: disable=too-many-format-args - ' of {}s'.format( - js_to_py_type(type_object['value'])) - if js_to_py_type(type_object['value']) != '' - else ''), - - # React's PropTypes.objectOf - objectOf=lambda: ( - 'dict with strings as keys and values of type {}' - ).format( - js_to_py_type(type_object['value'])), - - # React's PropTypes.shape - shape=lambda: 'dict containing keys {}.\n{}'.format( - ', '.join( - "'{}'".format(t) - for t in list(type_object['value'].keys())), - 'Those keys have the following types: \n{}'.format( - '\n'.join(create_prop_docstring( - prop_name=prop_name, - type_object=prop, - required=prop['required'], - description=prop.get('description', ''), - indent_num=1) - for prop_name, prop in - list(type_object['value'].items())))), - ) - - -def map_js_to_py_types_flow_types(type_object): - """Mapping from the Flow js types to the Python type""" - return dict( - array=lambda: 'list', - boolean=lambda: 'boolean', - number=lambda: 'number', - string=lambda: 'string', - Object=lambda: 'dict', - any=lambda: 'bool | number | str | dict | list', - Element=lambda: 'dash component', - Node=lambda: 'a list of or a singular dash ' - 'component, string or number', - - # React's PropTypes.oneOfType - union=lambda: '{}'.format( - ' | '.join( - '{}'.format(js_to_py_type(subType)) - for subType in type_object['elements'] - if js_to_py_type(subType) != '')), - - # Flow's Array type - Array=lambda: 'list{}'.format( - ' of {}s'.format( - js_to_py_type(type_object['elements'][0])) - if js_to_py_type(type_object['elements'][0]) != '' - else ''), - - # React's PropTypes.shape - signature=lambda indent_num: 'dict containing keys {}.\n{}'.format( - ', '.join("'{}'".format(d['key']) - for d in type_object['signature']['properties']), - '{}Those keys have the following types: \n{}'.format( - ' ' * indent_num, - '\n'.join( - create_prop_docstring( - prop_name=prop['key'], - type_object=prop['value'], - required=prop['value']['required'], - description=prop['value'].get('description', ''), - indent_num=indent_num, - is_flow_type=True) - for prop in type_object['signature']['properties']))), - ) - - -def js_to_py_type(type_object, is_flow_type=False, indent_num=0): - """ - Convert JS types to Python types for the component definition - - Parameters - ---------- - type_object: dict - react-docgen-generated prop type dictionary - is_flow_type: bool - Does the prop use Flow types? Otherwise, uses PropTypes - indent_num: int - Number of indents to use for the docstring for the prop - - Returns - ------- - str - Python type string - """ - js_type_name = type_object['name'] - js_to_py_types = map_js_to_py_types_flow_types(type_object=type_object) \ - if is_flow_type \ - else map_js_to_py_types_prop_types(type_object=type_object) - - if 'computed' in type_object and type_object['computed'] \ - or type_object.get('type', '') == 'function': - return '' - elif js_type_name in js_to_py_types: - if js_type_name == 'signature': # This is a Flow object w/ signature - return js_to_py_types[js_type_name](indent_num) - # All other types - return js_to_py_types[js_type_name]() - return '' - - -def generate_imports(project_shortname, components): - with open(os.path.join(project_shortname, '_imports_.py'), 'w') as f: - f.write(textwrap.dedent( - ''' - {} - - __all__ = [ - {} - ] - '''.format( - '\n'.join( - 'from .{0} import {0}'.format(x) for x in components), - ',\n'.join(' "{}"'.format(x) for x in components) - ) - ).lstrip()) - - -def generate_classes_files(project_shortname, metadata, *component_generators): - components = [] - for component_path, component_data in metadata.items(): - component_name = component_path.split('/')[-1].split('.')[0] - components.append(component_name) + varnames = func.__code__.co_varnames - for generator in component_generators: - generator( - component_name, - component_data['props'], - component_data['description'], - project_shortname + 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) - return components + # 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/component_generator.py b/dash/development/component_generator.py index 6c2f9bd842..8e04e0eab4 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -7,12 +7,12 @@ import os import argparse import shutil -import pkg_resources +import pkg_resources -from .base_component import generate_class_file -from .base_component import generate_imports -from .base_component import generate_classes_files +from ._py_components_generation import generate_class_file +from ._py_components_generation import generate_imports +from ._py_components_generation import generate_classes_files class _CombinedFormatter(argparse.ArgumentDefaultsHelpFormatter, diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 99a451fd29..968a3fa1d1 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -2,10 +2,12 @@ import json import os -from .base_component import generate_imports -from .base_component import generate_classes_files -from .base_component import generate_class -from .base_component import generate_class_file +from ._py_components_generation import ( + generate_class_file, + generate_imports, + generate_classes_files, + generate_class +) from .base_component import ComponentRegistry diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py index a43be1b898..9e725f5903 100644 --- a/tests/development/test_base_component.py +++ b/tests/development/test_base_component.py @@ -8,15 +8,10 @@ import plotly from dash.development.base_component import ( - generate_class, - generate_class_string, - generate_class_file, Component, - _explicitize_args, - js_to_py_type, - create_docstring, - parse_events -) + _explicitize_args) +from dash.development._py_components_generation import generate_class_string, generate_class_file, generate_class, \ + create_docstring, parse_events, js_to_py_type Component._prop_names = ('id', 'a', 'children', 'style', ) Component._type = 'TestComponent' diff --git a/tests/development/test_component_loader.py b/tests/development/test_component_loader.py index a4fb4423e1..7f3ce871fb 100644 --- a/tests/development/test_component_loader.py +++ b/tests/development/test_component_loader.py @@ -5,9 +5,9 @@ import unittest from dash.development.component_loader import load_components, generate_classes from dash.development.base_component import ( - generate_class, Component ) +from dash.development._py_components_generation import generate_class METADATA_PATH = 'metadata.json' diff --git a/tests/test_resources.py b/tests/test_resources.py index a25553abe5..f0214f45b2 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -1,7 +1,7 @@ import unittest import warnings from dash.resources import Scripts, Css -from dash.development.base_component import generate_class +from dash.development._py_components_generation import generate_class def generate_components(): From a4924483ab937b3b8f7bbd68f5b1aa56b037b1c5 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Mon, 26 Nov 2018 17:25:01 -0500 Subject: [PATCH 25/25] Update version and changelog. --- CHANGELOG.md | 4 ++++ dash/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e82d00ea4..fcf11203b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.31.0 - 2018-11-26 +## Added +- Combined `extract-meta` and python component files generation in a cli [#451](https://github.com/plotly/dash/pull/451) + ## 0.30.0 - 2018-11-14 ## Added - Hot reload from the browser [#362](https://github.com/plotly/dash/pull/362) diff --git a/dash/version.py b/dash/version.py index e187e0aa61..c3d10d7c49 100644 --- a/dash/version.py +++ b/dash/version.py @@ -1 +1 @@ -__version__ = '0.30.0' +__version__ = '0.31.0'