From e39bcf1be6d9b9b5249e12c78028e33d7287021a Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 5 Nov 2018 10:10:20 -0500 Subject: [PATCH 01/25] Initial version of updated code for R transpiler --- dash/development/base_component.py | 210 +++++++++++++++++++++++++++ dash/development/component_loader.py | 6 +- 2 files changed, 213 insertions(+), 3 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index c4998be300..e7150901e5 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -387,6 +387,120 @@ def __repr__(self): required_args = required_props(props) return c.format(**locals()) +def generate_class_string_r(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 = ''' + """{docstring}""" + + html{typename} <- function(..., {default_argtext}) { + + component <- list( + props = list( + {default_paramtext} + ), + type = '{typename}', + namespace = '{dash_html_components}', + propNames = c({list_of_valid_keys.strip("[]")}), + package = '{namespace}' + ) + + component$props <- filter_null(component$props) + component <- append_wildcard_props(component, wildcards = c({default_wildcards}), ...) + + structure(component, class = c('dash_component', 'list')) + } +''' + + 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 + # want to remove data-*, aria-* + events = '[' + ', '.join(parse_events(props)) + ']' + prop_keys = list(props.keys()) + default_paramtext = '' + default_wildcards = '' + wildcard_list = '' + for key in props: + if '*' in key: + wildcard_list.join(key) + if 'children' in props: + prop_keys.remove('children') + default_argtext = "children=NULL, " + # pylint: disable=unused-variable + argtext = 'children=children, **args' + else: + default_argtext = "" + argtext = '**args' + # in R, we set parameters with no defaults to NULL, here we'll do that if no default value exists + # the string which is used to define parameters and their default values is called default_argtext + default_wildcards += ", ".join( + [('\'{:s}\''.format(p)) + for p in prop_keys + if '*' in p] + ) + default_argtext += ", ".join( + [('{:s}={}'.format(p, props[p]['defaultValue']['value']) + if 'defaultValue' in props[p] else + '{:s}=NULL'.format(p)) + for p in prop_keys + if not p.endswith("-*") and + p not in keyword.kwlist and + p not in ['setProps']] + ) + default_paramtext += ", ".join( + [('{:s}={:s}'.format(p, p) + if p != "children" else + '{:s}=c(children, assert_valid_children(..., wildcards = c({:s})))'.format(p, default_wildcards)) + for p in props.keys() + if not p.endswith("-*") and + p not in keyword.kwlist and + p not in ['setProps']] + ) + required_args = required_props(props) + return c.format(**locals()) # pylint: disable=unused-argument def generate_class_file(typename, props, description, namespace): @@ -421,6 +535,37 @@ def generate_class_file(typename, props, description, namespace): f.write(import_string) f.write(class_string) +# pylint: disable=unused-argument +# pylint: disable=unused-argument +def generate_class_file_r(typename, props, description, namespace): + """ + Generate a R class file (.R) given a class string + + Parameters + ---------- + typename + props + description + namespace + + Returns + ------- + + """ + import_string =\ + "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + class_string = generate_class_string_r( + typename, + props, + description, + namespace + ) + file_name = "{:s}.R".format(typename) + + file_path = os.path.join(namespace, file_name) + with open(file_path, 'w') as f: + f.write(import_string) + f.write(class_string) # pylint: disable=unused-argument def generate_class(typename, props, description, namespace): @@ -507,6 +652,52 @@ def create_docstring(component_name, props, events, description): for p, prop in list(filter_props(props).items())), events=', '.join(events)) +def create_docstring_r(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) + + desctext = '' + desctext += "\n".join( + [('#\' @param {:s} {:s}'.format(p, props[p]['description'].replace('\n', ' '))) + for p in props.keys() + if not p.endswith("-*") and + p not in keyword.kwlist and + p not in ['setProps']] + ) + + return ( + """ +#' {name} component +#' @description See +#' @export +#' @param ... The children of this component and/or 'wildcards' of the form: `data-*` or `aria-*` +{desctext} + + """ + ).format( + name=component_name, + description=description, + desctext=desctext, + events=', '.join(events)) def parse_events(props): """ @@ -550,6 +741,25 @@ def parse_wildcards(props): list_of_valid_wildcard_attr_prefixes.append(wildcard_attr[:-1]) return list_of_valid_wildcard_attr_prefixes +def parse_wildcards_r(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): """ diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 74d2e557d4..90fd8aba34 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -1,9 +1,9 @@ import collections import json import os -from .base_component import generate_class -from .base_component import generate_class_file - +from dash.development.base_component import generate_class +#from .base_component import generate_class +from dash.development.base_component import generate_class_file def _get_metadata(metadata_path): # Start processing From 23bb710a69d687bac5ca23e4f8a6cbf2f32a4c35 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 5 Nov 2018 16:24:26 -0500 Subject: [PATCH 02/25] Fixed creation of R functions for components --- dash/development/base_component.py | 49 ++++++++++++++++++++-------- dash/development/component_loader.py | 16 +++++++-- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index e7150901e5..686c77600b 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -425,23 +425,23 @@ def generate_class_string_r(typename, props, description, namespace): c = ''' """{docstring}""" - html{typename} <- function(..., {default_argtext}) { + html{typename} <- function(..., {default_argtext}) {{ component <- list( props = list( {default_paramtext} ), type = '{typename}', - namespace = '{dash_html_components}', - propNames = c({list_of_valid_keys.strip("[]")}), - package = '{namespace}' + namespace = '{namespace}', + propNames = c({list_of_valid_keys}), + package = '{package_name}' ) component$props <- filter_null(component$props) component <- append_wildcard_props(component, wildcards = c({default_wildcards}), ...) structure(component, class = c('dash_component', 'list')) - } + }} ''' filtered_props = reorder_props(filter_props(props)) @@ -449,8 +449,10 @@ def generate_class_string_r(typename, props, description, namespace): 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()))) + list_of_valid_keys = list_of_valid_keys[1:-1] + package_name = make_package_name(namespace) # pylint: disable=unused-variable - docstring = create_docstring( + docstring = create_docstring_r( component_name=typename, props=filtered_props, events=parse_events(props), @@ -475,7 +477,6 @@ def generate_class_string_r(typename, props, description, namespace): default_argtext = "" argtext = '**args' # in R, we set parameters with no defaults to NULL, here we'll do that if no default value exists - # the string which is used to define parameters and their default values is called default_argtext default_wildcards += ", ".join( [('\'{:s}\''.format(p)) for p in prop_keys @@ -535,7 +536,6 @@ def generate_class_file(typename, props, description, namespace): f.write(import_string) f.write(class_string) -# pylint: disable=unused-argument # pylint: disable=unused-argument def generate_class_file_r(typename, props, description, namespace): """ @@ -590,6 +590,27 @@ def generate_class(typename, props, description, namespace): result = scope[typename] return result +def generate_class_r(typename, props, description, namespace): + """ + Generate a python class object given a class string + + Parameters + ---------- + typename + props + description + namespace + + Returns + ------- + + """ + string = generate_class_string_r(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): """ @@ -684,15 +705,12 @@ def create_docstring_r(component_name, props, events, description): p not in ['setProps']] ) - return ( - """ -#' {name} component + return('''#' {name} component #' @description See #' @export #' @param ... The children of this component and/or 'wildcards' of the form: `data-*` or `aria-*` {desctext} - - """ +''' ).format( name=component_name, description=description, @@ -1047,3 +1065,8 @@ 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 make_package_name(namestring): + # first, *rest = namestring.split('_') Python 3 + first, rest = namestring.split('_')[0], namestring.split('_')[1:] + return first + ''.join(word.capitalize() for word in rest) \ No newline at end of file diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 90fd8aba34..765d1e0ef2 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -1,9 +1,12 @@ import collections import json import os -from dash.development.base_component import generate_class -#from .base_component import generate_class -from dash.development.base_component import generate_class_file +#from dash.development.base_component import generate_class +from .base_component import generate_class +from .base_component import generate_class_file +from .base_component import generate_class_r +from .base_component import generate_class_file_r +#from dash.development.base_component import generate_class_file def _get_metadata(metadata_path): # Start processing @@ -94,8 +97,15 @@ def generate_classes(namespace, metadata_path='lib/metadata.json'): componentData['description'], namespace ) + generate_class_file_r( + name, + componentData['props'], + componentData['description'], + namespace + ) # Add an import statement for this component + # RK: need to add import statements in R namespace file also with open(imports_path, 'a') as f: f.write('from .{0:s} import {0:s}\n'.format(name)) From 9ce9509b339ba32b2a56bd0555508f02a9026d5d Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 5 Nov 2018 17:27:35 -0500 Subject: [PATCH 03/25] Fixed creation of R functions for components, triple quote issue --- dash/development/base_component.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 686c77600b..cc629279ba 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -422,8 +422,7 @@ def generate_class_string_r(typename, props, description, namespace): # it to be `null` or whether that was just the default value. # The solution might be to deal with default values better although # not all component authors will supply those. - c = ''' - """{docstring}""" + c = '''{docstring} html{typename} <- function(..., {default_argtext}) {{ From ed5d5dbfcfa1a8f098e97ec1fe61fe5fdb6e3dc0 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Thu, 8 Nov 2018 20:16:59 -0500 Subject: [PATCH 04/25] Edits related to PR comments --- dash/development/_all_keywords.py | 72 +++++++ dash/development/base_component.py | 273 ++++++++++++++++++--------- dash/development/component_loader.py | 16 +- 3 files changed, 271 insertions(+), 90 deletions(-) create mode 100644 dash/development/_all_keywords.py diff --git a/dash/development/_all_keywords.py b/dash/development/_all_keywords.py new file mode 100644 index 0000000000..5f9f554f2e --- /dev/null +++ b/dash/development/_all_keywords.py @@ -0,0 +1,72 @@ +# This is a set of Python keywords that cannot be used as prop names. +# Keywords for a particular version are obtained as follows: +# >>> import keyword +# >>> keyword.kwlist + +kwlist = set([ + 'and', + 'elif', + 'is', + 'global', + 'as', + 'in', + 'if', + 'from', + 'raise', + 'for', + 'except', + 'nonlocal', + 'pass', + 'finally', + 'print', + 'import', + 'True', + 'None', + 'return', + 'exec', + 'await', + 'else', + 'break', + 'not', + 'with', + 'class', + 'assert', + 'False', + 'yield', + 'try', + 'while', + 'continue', + 'del', + 'async', + 'or', + 'def', + 'lambda' +]) + +# This is a set of R reserved words that cannot be used as function argument names. +# +# Reserved words can be obtained from R's help pages by executing the statement below: +# > ?reserved + +r_keywords = set([ + 'if', + 'else', + 'repeat', + 'while', + 'function', + 'for', + 'in', + 'next', + 'break', + 'TRUE', + 'FALSE', + 'NULL', + 'Inf', + 'NaN', + 'NA', + 'NA_integer_', + 'NA_real_', + 'NA_complex_', + 'NA_character_', + '...' +]) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index cc629279ba..5a6223e953 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -2,8 +2,8 @@ import copy import os import inspect -import keyword +from ._all_keywords import kwlist, r_keywords def is_number(s): try: @@ -352,18 +352,14 @@ def __repr__(self): ''' 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: @@ -380,17 +376,25 @@ def __repr__(self): '{:s}=Component.UNDEFINED'.format(p)) for p in prop_keys if not p.endswith("-*") and - p not in keyword.kwlist and + p not in kwlist and p not in ['dashEvents', 'fireEvent', 'setProps']] + ['**kwargs'] ) required_args = required_props(props) - return c.format(**locals()) + return c.format(typename=typename, + docstring=docstring, + default_argtext=default_argtext, + list_of_valid_keys=list_of_valid_keys, + namespace=namespace, + list_of_valid_wildcard_attr_prefixes=list_of_valid_wildcard_attr_prefixes, + events=events, + required_args=required_args, + argtext=argtext) def generate_class_string_r(typename, props, description, namespace): """ - Dynamically generate class strings to have nicely formatted docstrings, - keyword arguments, and repr + Dynamically generate class strings to have nicely formatted documentation, + and function arguments Inspired by http://jameso.be/2013/08/06/namedtuple.html @@ -406,23 +410,7 @@ def generate_class_string_r(typename, props, description, namespace): 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 = '''{docstring} + c = '''{helptext} html{typename} <- function(..., {default_argtext}) {{ @@ -444,22 +432,24 @@ def generate_class_string_r(typename, props, description, namespace): ''' 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()))) + + # This strips the brackets from the keylist without importing the re module list_of_valid_keys = list_of_valid_keys[1:-1] - package_name = make_package_name(namespace) - # pylint: disable=unused-variable - docstring = create_docstring_r( + + # Here we convert from snake case to camel case + package_name = make_package_name_r(namespace) + + # There's no need to retain linefeeds in the R help text, will wrap automatically + helptext = create_helptext_r( component_name=typename, props=filtered_props, events=parse_events(props), - description=description).replace('\r\n', '\n') + description=description).replace('\r\n', ' ') - # pylint: disable=unused-variable - # want to remove data-*, aria-* - events = '[' + ', '.join(parse_events(props)) + ']' prop_keys = list(props.keys()) default_paramtext = '' default_wildcards = '' @@ -470,12 +460,10 @@ def generate_class_string_r(typename, props, description, namespace): if 'children' in props: prop_keys.remove('children') default_argtext = "children=NULL, " - # pylint: disable=unused-variable - argtext = 'children=children, **args' else: default_argtext = "" - argtext = '**args' - # in R, we set parameters with no defaults to NULL, here we'll do that if no default value exists + # in R, we set parameters with no defaults to NULL + # Here we'll do that if no default value exists default_wildcards += ", ".join( [('\'{:s}\''.format(p)) for p in prop_keys @@ -487,22 +475,28 @@ def generate_class_string_r(typename, props, description, namespace): '{:s}=NULL'.format(p)) for p in prop_keys if not p.endswith("-*") and - p not in keyword.kwlist and + p not in r_keywords and p not in ['setProps']] ) + # pylint: disable=C0301 default_paramtext += ", ".join( - [('{:s}={:s}'.format(p, p) - if p != "children" else - '{:s}=c(children, assert_valid_children(..., wildcards = c({:s})))'.format(p, default_wildcards)) - for p in props.keys() - if not p.endswith("-*") and - p not in keyword.kwlist and - p not in ['setProps']] + [('{:s}={:s}'.format(p, p) + if p != "children" else + '{:s}=c(children, assert_valid_children(..., wildcards = c({:s})))'.format(p, default_wildcards)) + for p in props.keys() + if not p.endswith("-*") and + p not in r_keywords and + p not in ['setProps']] ) - required_args = required_props(props) - return c.format(**locals()) + return c.format(helptext=helptext, + typename=typename, + default_argtext=default_argtext, + default_paramtext=default_paramtext, + namespace=namespace, + list_of_valid_keys=list_of_valid_keys, + package_name=package_name, + default_wildcards=default_wildcards) -# pylint: disable=unused-argument def generate_class_file(typename, props, description, namespace): """ Generate a python class file (.py) given a class string @@ -561,11 +555,139 @@ def generate_class_file_r(typename, props, description, namespace): ) file_name = "{:s}.R".format(typename) - file_path = os.path.join(namespace, file_name) + if not os.path.exists('R'): + os.makedirs('R') + + file_path = os.path.join('R', file_name) with open(file_path, 'w') as f: f.write(import_string) f.write(class_string) +def generate_rpkg(name, pkg_data, namespace): + ''' + Generate documents for R package creation + + Parameters + ---------- + name + pkg_data + namespace + + Returns + ------- + + ''' + # Leverage package.json to import specifics which are also applicable + # to R package that we're generating here + package_name = make_package_name_r(namespace) + package_description = pkg_data['description'] + package_version = pkg_data['version'] + package_issues = pkg_data['bugs']['url'] + package_url = pkg_data['homepage'] + + package_author = pkg_data['author'] + + # The following approach avoids use of regex, but there are probably better ways! + package_author_no_email = package_author.split(" <")[0] + ' [aut]' + + if not (os.path.isfile('LICENSE') or os.path.isfile('LICENSE.txt')): + package_license = pkg_data['license'] + else: + package_license = pkg_data['license'] + ' + file LICENSE' + + import_string =\ + '# AUTO GENERATED FILE - DO NOT EDIT\n\n' + + # To remove once we've sorted out how to handle internals + temp_string =\ + '''export(filter_null) +export(assert_valid_children) +export(append_wildcard_props) +export(names2) +export(`%||%`) +export(assert_no_names) +''' + + description_string = \ + '''Package: {package_name} +Title: {package_description} +Version: {package_version} +Authors @R: as.person(c({package_author})) +Description: {package_description} +Suggests: testthat, roxygen2 +License: {package_license} +URL: {package_url} +BugReports: {package_issues} +Encoding: UTF-8 +LazyData: true +Author: {package_author_no_email} +Maintainer: {package_author} +''' + + description_string = description_string.format(package_name=package_name, + package_description=package_description, + package_version=package_version, + package_author=package_author, + package_license=package_license, + package_url=package_url, + package_issues=package_issues, + package_author_no_email=package_author_no_email) + + rbuild_ignore_string = '''# ignore JS config files/folders +node_modules/ +coverage/ +src/ +lib/ +.babelrc +.builderrc +.eslintrc +.npmignore + +# demo folder has special meaning in R +# this should hopefully make it still +# allow for the possibility to make R demos +demo/*.js +demo/*.html +demo/*.css + +# ignore python files/folders +setup.py +usage.py +setup.py +requirements.txt +MANIFEST.in +CHANGELOG.md +test/ +# CRAN has weird LICENSE requirements +LICENSE.txt +^.*\.Rproj$ +^\.Rproj\.user$ + +# ignore venv +venv/ +''' + + # pylint: disable=unused-variable + if not name.endswith('-*') and \ + str(name) not in r_keywords and \ + str(name) not in ['setProps', 'children']: + export_string = 'export(html{:s})\n'.format(name) + + if not os.path.isfile('NAMESPACE'): + with open('NAMESPACE', 'w') as f: + f.write(import_string) + f.write(export_string) + f.write(temp_string) + else: + with open('NAMESPACE', 'a') as f: + f.write(export_string) + + with open('DESCRIPTION', 'w') as f2: + f2.write(description_string) + + with open('.Rbuildignore', 'w') as f3: + f3.write(rbuild_ignore_string) + # pylint: disable=unused-argument def generate_class(typename, props, description, namespace): """ @@ -672,9 +794,9 @@ def create_docstring(component_name, props, events, description): for p, prop in list(filter_props(props).items())), events=', '.join(events)) -def create_docstring_r(component_name, props, events, description): +def create_helptext_r(component_name, props, events, description): """ - Create the Dash component docstring + Create the Dash component help text for R version of Dash components Parameters ---------- @@ -690,17 +812,17 @@ def create_docstring_r(component_name, props, events, description): Returns ------- str - Dash component docstring + Dash component help text """ # Ensure props are ordered with children first props = reorder_props(props=props) desctext = '' - desctext += "\n".join( - [('#\' @param {:s} {:s}'.format(p, props[p]['description'].replace('\n', ' '))) + desctext = "\n".join( + [('#\' @param {:s} {:s}'.format(p, props[p]['description'])) for p in props.keys() if not p.endswith("-*") and - p not in keyword.kwlist and + p not in r_keywords and p not in ['setProps']] ) @@ -709,12 +831,9 @@ def create_docstring_r(component_name, props, events, description): #' @export #' @param ... The children of this component and/or 'wildcards' of the form: `data-*` or `aria-*` {desctext} -''' - ).format( +''').format( name=component_name, - description=description, - desctext=desctext, - events=', '.join(events)) + desctext=desctext) def parse_events(props): """ @@ -758,26 +877,6 @@ def parse_wildcards(props): list_of_valid_wildcard_attr_prefixes.append(wildcard_attr[:-1]) return list_of_valid_wildcard_attr_prefixes -def parse_wildcards_r(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 @@ -1065,7 +1164,9 @@ def js_to_py_type(type_object, is_flow_type=False, indent_num=0): return js_to_py_types[js_type_name]() return '' -def make_package_name(namestring): - # first, *rest = namestring.split('_') Python 3 - first, rest = namestring.split('_')[0], namestring.split('_')[1:] - return first + ''.join(word.capitalize() for word in rest) \ No newline at end of file +# This converts a string from snake case to camel case +# Not required for R package name to be in camel case, +# but probably more conventional this way +def make_package_name_r(namestring): + first, rest = namestring.split('_')[0], namestring.split('_')[1:] + return first + ''.join(word.capitalize() for word in rest) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 765d1e0ef2..bd9bcf641f 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -1,12 +1,11 @@ import collections import json import os -#from dash.development.base_component import generate_class from .base_component import generate_class from .base_component import generate_class_file from .base_component import generate_class_r from .base_component import generate_class_file_r -#from dash.development.base_component import generate_class_file +from .base_component import generate_rpkg def _get_metadata(metadata_path): # Start processing @@ -59,7 +58,7 @@ def load_components(metadata_path, return components -def generate_classes(namespace, metadata_path='lib/metadata.json'): +def generate_classes(namespace, metadata_path='lib/metadata.json', pkgjson_path='package.json'): """Load React component metadata into a format Dash can parse, then create python class files. @@ -75,12 +74,17 @@ def generate_classes(namespace, metadata_path='lib/metadata.json'): """ data = _get_metadata(metadata_path) + pkg_data = _get_metadata(pkgjson_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) + # Remove the R NAMESPACE file if it exists + if os.path.isfile('NAMESPACE'): + os.remove('NAMESPACE') + # Iterate over each property name (which is a path to the component) for componentPath in data: componentData = data[componentPath] @@ -103,9 +107,13 @@ def generate_classes(namespace, metadata_path='lib/metadata.json'): componentData['description'], namespace ) + generate_rpkg( + name, + pkg_data, + namespace + ) # Add an import statement for this component - # RK: need to add import statements in R namespace file also with open(imports_path, 'a') as f: f.write('from .{0:s} import {0:s}\n'.format(name)) From 3df1cd6a53c1f29a89f88db9e6f15148b2192bb1 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Fri, 9 Nov 2018 09:44:31 -0500 Subject: [PATCH 05/25] Edited _all_keywords.py so R/Py kwlists similarly named. --- dash/development/_all_keywords.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/development/_all_keywords.py b/dash/development/_all_keywords.py index 5f9f554f2e..550c1d9fa0 100644 --- a/dash/development/_all_keywords.py +++ b/dash/development/_all_keywords.py @@ -3,7 +3,7 @@ # >>> import keyword # >>> keyword.kwlist -kwlist = set([ +python_keywords = set([ 'and', 'elif', 'is', From 451283fda88be32b8bf5cd3ad3ebd28b0252687a Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Fri, 9 Nov 2018 09:45:48 -0500 Subject: [PATCH 06/25] Updated base_component.py kwlist >> python_keywords --- dash/development/base_component.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 5a6223e953..5c519d6f1e 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -3,7 +3,7 @@ import os import inspect -from ._all_keywords import kwlist, r_keywords +from ._all_keywords import python_keywords, r_keywords def is_number(s): try: @@ -376,7 +376,7 @@ def __repr__(self): '{:s}=Component.UNDEFINED'.format(p)) for p in prop_keys if not p.endswith("-*") and - p not in kwlist and + p not in python_keywords and p not in ['dashEvents', 'fireEvent', 'setProps']] + ['**kwargs'] ) From 59985457361d0fdf99f2690b29bf77b06299cb30 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Fri, 9 Nov 2018 10:45:07 -0500 Subject: [PATCH 07/25] Attempting to resolve pylint warnings --- dash/development/base_component.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 5c519d6f1e..b4c76e19d1 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -391,6 +391,7 @@ def __repr__(self): required_args=required_args, argtext=argtext) +# pylint: disable=R0914 def generate_class_string_r(typename, props, description, namespace): """ Dynamically generate class strings to have nicely formatted documentation, @@ -433,8 +434,6 @@ def generate_class_string_r(typename, props, description, namespace): filtered_props = reorder_props(filter_props(props)) - list_of_valid_wildcard_attr_prefixes = repr(parse_wildcards(props)) - list_of_valid_keys = repr(list(map(str, filtered_props.keys()))) # This strips the brackets from the keylist without importing the re module @@ -529,7 +528,6 @@ def generate_class_file(typename, props, description, namespace): f.write(import_string) f.write(class_string) -# pylint: disable=unused-argument def generate_class_file_r(typename, props, description, namespace): """ Generate a R class file (.R) given a class string @@ -563,6 +561,7 @@ def generate_class_file_r(typename, props, description, namespace): f.write(import_string) f.write(class_string) +# pylint: disable=R0914 def generate_rpkg(name, pkg_data, namespace): ''' Generate documents for R package creation @@ -633,7 +632,7 @@ def generate_rpkg(name, pkg_data, namespace): package_issues=package_issues, package_author_no_email=package_author_no_email) - rbuild_ignore_string = '''# ignore JS config files/folders + rbuild_ignore_string = r'''# ignore JS config files/folders node_modules/ coverage/ src/ @@ -831,9 +830,8 @@ def create_helptext_r(component_name, props, events, description): #' @export #' @param ... The children of this component and/or 'wildcards' of the form: `data-*` or `aria-*` {desctext} -''').format( - name=component_name, - desctext=desctext) +''').format(name=component_name, + desctext=desctext) def parse_events(props): """ From 660694356d2cd42c0c338dbc8ab1313e65bb2afa Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Fri, 9 Nov 2018 10:49:49 -0500 Subject: [PATCH 08/25] Resolve pylint warning by removing unused generate_class_r code --- dash/development/base_component.py | 22 ---------------------- dash/development/component_loader.py | 1 - 2 files changed, 23 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index b4c76e19d1..501c11ce64 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -710,28 +710,6 @@ def generate_class(typename, props, description, namespace): result = scope[typename] return result -def generate_class_r(typename, props, description, namespace): - """ - Generate a python class object given a class string - - Parameters - ---------- - typename - props - description - namespace - - Returns - ------- - - """ - string = generate_class_string_r(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 diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index bd9bcf641f..b145e5e523 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -3,7 +3,6 @@ import os from .base_component import generate_class from .base_component import generate_class_file -from .base_component import generate_class_r from .base_component import generate_class_file_r from .base_component import generate_rpkg From 4575b9fd8d20eb361d5afab9b706e686477fd010 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sat, 10 Nov 2018 15:23:15 -0500 Subject: [PATCH 09/25] Updated base_component.py --- dash/development/base_component.py | 96 ++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 501c11ce64..aa6517473d 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -2,9 +2,53 @@ import copy import os import inspect +import abc +import sys +import six from ._all_keywords import python_keywords, r_keywords + +# pylint: disable=no-init,too-few-public-methods +class ComponentRegistry: + """Holds a registry of the namespaces used by components.""" + + registry = set() + __dist_cache = {} + + @classmethod + def get_resources(cls, resource_name): + cached = cls.__dist_cache.get(resource_name) + + if cached: + return cached + + cls.__dist_cache[resource_name] = resources = [] + + for module_name in cls.registry: + module = sys.modules[module_name] + resources.extend(getattr(module, resource_name, [])) + + return resources + + +class ComponentMeta(abc.ABCMeta): + + # pylint: disable=arguments-differ + def __new__(mcs, name, bases, attributes): + component = abc.ABCMeta.__new__(mcs, name, bases, attributes) + module = attributes['__module__'].split('.')[0] + if name == 'Component' or module == 'builtins': + # Don't do the base component + # and the components loaded dynamically by load_component + # as it doesn't have the namespace. + return component + + ComponentRegistry.registry.add(module) + + return component + + def is_number(s): try: float(s) @@ -53,6 +97,7 @@ def wrapper(*args, **kwargs): return wrapper +@six.add_metaclass(ComponentMeta) class Component(collections.MutableMapping): class _UNDEFINED(object): def __repr__(self): @@ -352,14 +397,18 @@ def __repr__(self): ''' 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: @@ -376,7 +425,7 @@ def __repr__(self): '{:s}=Component.UNDEFINED'.format(p)) for p in prop_keys if not p.endswith("-*") and - p not in python_keywords and + p not in kwlist and p not in ['dashEvents', 'fireEvent', 'setProps']] + ['**kwargs'] ) @@ -464,28 +513,28 @@ def generate_class_string_r(typename, props, description, namespace): # in R, we set parameters with no defaults to NULL # Here we'll do that if no default value exists default_wildcards += ", ".join( - [('\'{:s}\''.format(p)) + ('\'{:s}\''.format(p)) for p in prop_keys - if '*' in p] + if '*' in p ) default_argtext += ", ".join( - [('{:s}={}'.format(p, props[p]['defaultValue']['value']) + ('{:s}={}'.format(p, props[p]['defaultValue']['value']) if 'defaultValue' in props[p] else '{:s}=NULL'.format(p)) for p in prop_keys if not p.endswith("-*") and p not in r_keywords and - p not in ['setProps']] + p not in ['setProps'] ) # pylint: disable=C0301 default_paramtext += ", ".join( - [('{:s}={:s}'.format(p, p) + ('{:s}={:s}'.format(p, p) if p != "children" else '{:s}=c(children, assert_valid_children(..., wildcards = c({:s})))'.format(p, default_wildcards)) for p in props.keys() if not p.endswith("-*") and p not in r_keywords and - p not in ['setProps']] + p not in ['setProps'] ) return c.format(helptext=helptext, typename=typename, @@ -496,6 +545,7 @@ def generate_class_string_r(typename, props, description, namespace): package_name=package_name, default_wildcards=default_wildcards) +# pylint: disable=unused-argument def generate_class_file(typename, props, description, namespace): """ Generate a python class file (.py) given a class string @@ -561,8 +611,15 @@ def generate_class_file_r(typename, props, description, namespace): f.write(import_string) f.write(class_string) +# pylint: disable=unused-variable +def generate_export_string_r(name): + if not name.endswith('-*') and \ + str(name) not in r_keywords and \ + str(name) not in ['setProps', 'children']: + return 'export(html{:s})\n'.format(name) + # pylint: disable=R0914 -def generate_rpkg(name, pkg_data, namespace): +def generate_rpkg(pkg_data, namespace, export_string): ''' Generate documents for R package creation @@ -597,7 +654,6 @@ def generate_rpkg(name, pkg_data, namespace): import_string =\ '# AUTO GENERATED FILE - DO NOT EDIT\n\n' - # To remove once we've sorted out how to handle internals temp_string =\ '''export(filter_null) export(assert_valid_children) @@ -666,20 +722,10 @@ def generate_rpkg(name, pkg_data, namespace): venv/ ''' - # pylint: disable=unused-variable - if not name.endswith('-*') and \ - str(name) not in r_keywords and \ - str(name) not in ['setProps', 'children']: - export_string = 'export(html{:s})\n'.format(name) - - if not os.path.isfile('NAMESPACE'): - with open('NAMESPACE', 'w') as f: - f.write(import_string) - f.write(export_string) - f.write(temp_string) - else: - with open('NAMESPACE', 'a') as f: - f.write(export_string) + with open('NAMESPACE', 'w') as f: + f.write(import_string) + f.write(export_string) + f.write(temp_string) with open('DESCRIPTION', 'w') as f2: f2.write(description_string) @@ -710,6 +756,7 @@ def generate_class(typename, props, description, namespace): result = scope[typename] return result + def required_props(props): """ Pull names of required props from the props object @@ -806,7 +853,7 @@ def create_helptext_r(component_name, props, events, description): return('''#' {name} component #' @description See #' @export -#' @param ... The children of this component and/or 'wildcards' of the form: `data-*` or `aria-*` +#' @param ... The children of this component and/or 'wildcards' of the form: 'data-*' or 'aria-*' {desctext} ''').format(name=component_name, desctext=desctext) @@ -853,6 +900,7 @@ def parse_wildcards(props): 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 From 9bb76031194e601abc85991b8e19477331bf29d2 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sat, 10 Nov 2018 15:23:32 -0500 Subject: [PATCH 10/25] Updated component_loader.py --- dash/development/component_loader.py | 76 +++++++++++++++++++++------- 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index b145e5e523..a3ad540903 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -3,7 +3,9 @@ import os from .base_component import generate_class from .base_component import generate_class_file +from .base_component import ComponentRegistry from .base_component import generate_class_file_r +from .base_component import generate_export_string_r from .base_component import generate_rpkg def _get_metadata(metadata_path): @@ -31,6 +33,8 @@ def load_components(metadata_path, `type`, `valid_kwargs`, and `setup`. """ + # Register the component lib for index include. + ComponentRegistry.registry.add(namespace) components = [] data = _get_metadata(metadata_path) @@ -57,7 +61,7 @@ def load_components(metadata_path, return components -def generate_classes(namespace, metadata_path='lib/metadata.json', pkgjson_path='package.json'): +def generate_classes(namespace, metadata_path='lib/metadata.json'): """Load React component metadata into a format Dash can parse, then create python class files. @@ -73,17 +77,12 @@ def generate_classes(namespace, metadata_path='lib/metadata.json', pkgjson_path= """ data = _get_metadata(metadata_path) - pkg_data = _get_metadata(pkgjson_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) - # Remove the R NAMESPACE file if it exists - if os.path.isfile('NAMESPACE'): - os.remove('NAMESPACE') - # Iterate over each property name (which is a path to the component) for componentPath in data: componentData = data[componentPath] @@ -100,17 +99,6 @@ def generate_classes(namespace, metadata_path='lib/metadata.json', pkgjson_path= componentData['description'], namespace ) - generate_class_file_r( - name, - componentData['props'], - componentData['description'], - namespace - ) - generate_rpkg( - name, - pkg_data, - namespace - ) # Add an import statement for this component with open(imports_path, 'a') as f: @@ -124,3 +112,57 @@ def generate_classes(namespace, metadata_path='lib/metadata.json', pkgjson_path= array_string += ' "{:s}",\n'.format(a) array_string += ']\n' f.write('\n\n__all__ = {:s}'.format(array_string)) + +def generate_classes_r(namespace, metadata_path='lib/metadata.json', pkgjson_path='package.json'): + """Load React component metadata into a format Dash can parse, + then create python class files. + + 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). + + Returns: + """ + + data = _get_metadata(metadata_path) + pkg_data = _get_metadata(pkgjson_path) + imports_path = os.path.join(namespace, '_imports_.py') + export_string = '' + + # Make sure the file doesn't exist, as we use append write + if os.path.exists(imports_path): + os.remove(imports_path) + + # Remove the R NAMESPACE file if it exists, this will be repopulated + if os.path.isfile('NAMESPACE'): + os.remove('NAMESPACE') + + # 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] + + export_string += generate_export_string_r(name) + + generate_class_file_r( + name, + componentData['props'], + componentData['description'], + namespace + ) + + generate_rpkg( + pkg_data, + namespace, + export_string + ) \ No newline at end of file From f4cd84489ee419f57b0af730cfdeb3e50e27a24e Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sat, 10 Nov 2018 15:45:27 -0500 Subject: [PATCH 11/25] Updated documentation for generate_classes_r --- dash/development/component_loader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index a3ad540903..8a871febb2 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -117,7 +117,7 @@ def generate_classes_r(namespace, metadata_path='lib/metadata.json', pkgjson_pat """Load React component metadata into a format Dash can parse, then create python class files. - Usage: generate_classes() + Usage: generate_classes_r() Keyword arguments: namespace -- name of the generated python package (also output dir) @@ -125,6 +125,9 @@ def generate_classes_r(namespace, metadata_path='lib/metadata.json', pkgjson_pat metadata_path -- a path to a JSON file created by [`react-docgen`](https://github.com/reactjs/react-docgen). + pkgjson_path -- a path to a JSON file created by + [`cookiecutter`](https://github.com/audreyr/cookiecutter). + Returns: """ From aae11d6cffbb6b58a0be3e7c1b98f48e72bd4c46 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sun, 11 Nov 2018 02:16:00 -0500 Subject: [PATCH 12/25] Updated base_component.py to support .Rd without roxygen2 --- dash/development/base_component.py | 141 ++++++++++++++++++----------- 1 file changed, 89 insertions(+), 52 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 5c6d54c659..d7051d0f8a 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -459,9 +459,13 @@ def generate_class_string_r(typename, props, description, namespace): string """ - c = '''{helptext} - html{typename} <- function(..., {default_argtext}) {{ + if namespace == 'dash_html_components': + prefix = 'html' + else: + prefix = '' + + c = '''{prefix}{typename} <- function(..., {default_argtext}) {{ component <- list( props = list( @@ -490,13 +494,6 @@ def generate_class_string_r(typename, props, description, namespace): # Here we convert from snake case to camel case package_name = make_package_name_r(namespace) - # There's no need to retain linefeeds in the R help text, will wrap automatically - helptext = create_helptext_r( - component_name=typename, - props=filtered_props, - events=parse_events(props), - description=description).replace('\r\n', ' ') - prop_keys = list(props.keys()) default_paramtext = '' default_wildcards = '' @@ -535,7 +532,7 @@ def generate_class_string_r(typename, props, description, namespace): p not in r_keywords and p not in ['setProps'] ) - return c.format(helptext=helptext, + return c.format(prefix=prefix, typename=typename, default_argtext=default_argtext, default_paramtext=default_paramtext, @@ -577,6 +574,80 @@ def generate_class_file(typename, props, description, namespace): f.write(import_string) f.write(class_string) +def generate_help_file_r(typename, props, namespace): + """ + Generate a R documentation file (.Rd) given component name and properties + + Parameters + ---------- + typename + props + + Returns + ------- + + + """ + if not os.path.exists('man'): + os.makedirs('man') + + if namespace == 'dash_html_components': + file_name = "html{:s}.Rd".format(typename) + prefix = 'html' + else: + file_name = "{:s}.Rd".format(typename) + prefix = '' + + prop_keys = list(props.keys()) + + default_argtext = '' + item_text = '' + + # Ensure props are ordered with children first + props = reorder_props(props=props) + + default_argtext += ", ".join( + ('{:s}={}'.format(p, props[p]['defaultValue']['value']) + if 'defaultValue' in props[p] else + '{:s}=NULL'.format(p)) + for p in prop_keys + if not p.endswith("-*") and + p not in r_keywords and + p not in ['setProps'] + ) + + item_text += "\n\n".join( + ('\\item{{{:s}}}{{{:s}}}'.format(p, props[p]['description'])) + for p in prop_keys + if not p.endswith("-*") and + p not in r_keywords and + p not in ['setProps'] + ) + + help_string = '''% Auto-generated: do not edit by hand +\\name{{{prefix}{typename}}} +\\alias{{{prefix}{typename}}} +\\title{{{typename} component}} +\\usage{{ +(..., {default_argtext}) +}} +\\arguments{{ +{item_text} +}} +\\description{{ +See +}} + ''' + + file_path = os.path.join('man', file_name) + with open(file_path, 'w') as f: + f.write(help_string.format( + prefix=prefix, + typename=typename, + default_argtext=default_argtext, + item_text=item_text + )) + def generate_class_file_r(typename, props, description, namespace): """ Generate a R class file (.R) given a class string @@ -592,6 +663,11 @@ def generate_class_file_r(typename, props, description, namespace): ------- """ + if namespace == 'dash_html_components': + prefix = 'html' + else: + prefix = '' + import_string =\ "# AUTO GENERATED FILE - DO NOT EDIT\n\n" class_string = generate_class_string_r( @@ -600,7 +676,7 @@ def generate_class_file_r(typename, props, description, namespace): description, namespace ) - file_name = "{:s}.R".format(typename) + file_name = "{:s}{:s}.R".format(prefix, typename) if not os.path.exists('R'): os.makedirs('R') @@ -618,7 +694,8 @@ def generate_export_string_r(name): return 'export(html{:s})\n'.format(name) # pylint: disable=R0914 -def generate_rpkg(pkg_data, namespace, export_string): +def generate_rpkg(pkg_data, + namespace, export_string): ''' Generate documents for R package creation @@ -814,46 +891,6 @@ def create_docstring(component_name, props, events, description): for p, prop in list(filter_props(props).items())), events=', '.join(events)) -def create_helptext_r(component_name, props, events, description): - """ - Create the Dash component help text for R version of Dash components - - 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 help text - """ - # Ensure props are ordered with children first - props = reorder_props(props=props) - - desctext = '' - desctext = "\n".join( - [('#\' @param {:s} {:s}'.format(p, props[p]['description'])) - for p in props.keys() - if not p.endswith("-*") and - p not in r_keywords and - p not in ['setProps']] - ) - - return('''#' {name} component -#' @description See -#' @export -#' @param ... The children of this component and/or 'wildcards' of the form: 'data-*' or 'aria-*' -{desctext} -''').format(name=component_name, - desctext=desctext) - def parse_events(props): """ Pull out the dashEvents from the Component props From 62bf001498ba39de0fe8ee143f7d8dfd77e21efe Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Sun, 11 Nov 2018 02:16:38 -0500 Subject: [PATCH 13/25] Updated component_loader.py to import generate_help_file_r --- dash/development/component_loader.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 8a871febb2..7da148819e 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -5,6 +5,7 @@ from .base_component import generate_class_file from .base_component import ComponentRegistry from .base_component import generate_class_file_r +from .base_component import generate_help_file_r from .base_component import generate_export_string_r from .base_component import generate_rpkg @@ -163,6 +164,12 @@ def generate_classes_r(namespace, metadata_path='lib/metadata.json', pkgjson_pat componentData['description'], namespace ) + + generate_help_file_r( + name, + componentData['props'], + namespace + ) generate_rpkg( pkg_data, From 89b7b85c5e1f1042663f1108501fb4af348beeec Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 12 Nov 2018 11:57:52 -0500 Subject: [PATCH 14/25] Updated component_loader.py and base_component.py --- dash/development/base_component.py | 22 ++++++++++++++-------- dash/development/component_loader.py | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index d7051d0f8a..6558fed6ff 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -484,7 +484,7 @@ def generate_class_string_r(typename, props, description, namespace): }} ''' - filtered_props = reorder_props(filter_props(props)) + filtered_props = filter_props(props) list_of_valid_keys = repr(list(map(str, filtered_props.keys()))) @@ -495,17 +495,16 @@ def generate_class_string_r(typename, props, description, namespace): package_name = make_package_name_r(namespace) prop_keys = list(props.keys()) + default_paramtext = '' + default_argtext = '' default_wildcards = '' wildcard_list = '' + for key in props: if '*' in key: wildcard_list.join(key) - if 'children' in props: - prop_keys.remove('children') - default_argtext = "children=NULL, " - else: - default_argtext = "" + # in R, we set parameters with no defaults to NULL # Here we'll do that if no default value exists default_wildcards += ", ".join( @@ -522,6 +521,10 @@ def generate_class_string_r(typename, props, description, namespace): p not in r_keywords and p not in ['setProps'] ) + + if 'children' in props: + prop_keys.remove('children') + # pylint: disable=C0301 default_paramtext += ", ".join( ('{:s}={:s}'.format(p, p) @@ -629,13 +632,13 @@ def generate_help_file_r(typename, props, namespace): \\alias{{{prefix}{typename}}} \\title{{{typename} component}} \\usage{{ -(..., {default_argtext}) +{prefix}{typename}(..., {default_argtext}) }} \\arguments{{ {item_text} }} \\description{{ -See +{typename} component }} ''' @@ -726,6 +729,9 @@ def generate_rpkg(pkg_data, package_license = pkg_data['license'] else: package_license = pkg_data['license'] + ' + file LICENSE' + # R requires that the LICENSE.txt file be named LICENSE + if not os.path.isfile('LICENSE'): + os.symlink("LICENSE.txt", "LICENSE") import_string =\ '# AUTO GENERATED FILE - DO NOT EDIT\n\n' diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 7da148819e..7ce5c6b157 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -137,7 +137,7 @@ def generate_classes_r(namespace, metadata_path='lib/metadata.json', pkgjson_pat imports_path = os.path.join(namespace, '_imports_.py') export_string = '' - # Make sure the file doesn't exist, as we use append write + # Make sure the file doesn't exist, as we use append to write lines if os.path.exists(imports_path): os.remove(imports_path) From 664e5169a53a802fe9f0cb5b93e54619a07768e2 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Mon, 12 Nov 2018 13:54:28 -0500 Subject: [PATCH 15/25] Modified base_component.py to address propNames resolution problem --- dash/development/base_component.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 6558fed6ff..4d6b0b5123 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -473,7 +473,7 @@ def generate_class_string_r(typename, props, description, namespace): ), type = '{typename}', namespace = '{namespace}', - propNames = c({list_of_valid_keys}), + propNames = c({prop_names}), package = '{package_name}' ) @@ -484,13 +484,6 @@ def generate_class_string_r(typename, props, description, namespace): }} ''' - filtered_props = filter_props(props) - - list_of_valid_keys = repr(list(map(str, filtered_props.keys()))) - - # This strips the brackets from the keylist without importing the re module - list_of_valid_keys = list_of_valid_keys[1:-1] - # Here we convert from snake case to camel case package_name = make_package_name_r(namespace) @@ -499,11 +492,13 @@ def generate_class_string_r(typename, props, description, namespace): default_paramtext = '' default_argtext = '' default_wildcards = '' - wildcard_list = '' - for key in props: - if '*' in key: - wildcard_list.join(key) + # Produce a string with all property names other than WCs + prop_names = ", ".join( + ('\'{:s}\''.format(p)) + for p in prop_keys + if '*' not in p + ) # in R, we set parameters with no defaults to NULL # Here we'll do that if no default value exists @@ -512,6 +507,7 @@ def generate_class_string_r(typename, props, description, namespace): for p in prop_keys if '*' in p ) + default_argtext += ", ".join( ('{:s}={}'.format(p, props[p]['defaultValue']['value']) if 'defaultValue' in props[p] else @@ -540,7 +536,7 @@ def generate_class_string_r(typename, props, description, namespace): default_argtext=default_argtext, default_paramtext=default_paramtext, namespace=namespace, - list_of_valid_keys=list_of_valid_keys, + prop_names=prop_names, package_name=package_name, default_wildcards=default_wildcards) From 3bf21b340bd876904eb5bbd32e90d351029a2e2c Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Fri, 23 Nov 2018 14:33:59 -0500 Subject: [PATCH 16/25] Updated keyword list to include R reserved words --- dash/development/_all_keywords.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/dash/development/_all_keywords.py b/dash/development/_all_keywords.py index 76842b56af..550c1d9fa0 100644 --- a/dash/development/_all_keywords.py +++ b/dash/development/_all_keywords.py @@ -3,7 +3,7 @@ # >>> import keyword # >>> keyword.kwlist -kwlist = set([ +python_keywords = set([ 'and', 'elif', 'is', @@ -42,3 +42,31 @@ 'def', 'lambda' ]) + +# This is a set of R reserved words that cannot be used as function argument names. +# +# Reserved words can be obtained from R's help pages by executing the statement below: +# > ?reserved + +r_keywords = set([ + 'if', + 'else', + 'repeat', + 'while', + 'function', + 'for', + 'in', + 'next', + 'break', + 'TRUE', + 'FALSE', + 'NULL', + 'Inf', + 'NaN', + 'NA', + 'NA_integer_', + 'NA_real_', + 'NA_complex_', + 'NA_character_', + '...' +]) From 84a6745d029a2d7179b207ed982be8a06b42b24c Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Fri, 23 Nov 2018 14:35:43 -0500 Subject: [PATCH 17/25] Changes to support transpiling of JSON>>R within Python --- dash/development/base_component.py | 367 ++++++++++++++++++++++++++++- 1 file changed, 364 insertions(+), 3 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 789253a3f2..036cba99c9 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -6,7 +6,7 @@ import sys import six -from ._all_keywords import kwlist +from ._all_keywords import python_keywords, r_keywords # pylint: disable=no-init,too-few-public-methods @@ -425,13 +425,141 @@ def __repr__(self): '{:s}=Component.UNDEFINED'.format(p)) for p in prop_keys if not p.endswith("-*") and - p not in kwlist and + p not in python_keywords and p not in ['dashEvents', 'fireEvent', 'setProps']] + ['**kwargs'] ) required_args = required_props(props) - return c.format(**locals()) + return c.format(typename=typename, + docstring=docstring, + default_argtext=default_argtext, + list_of_valid_keys=list_of_valid_keys, + namespace=namespace, + list_of_valid_wildcard_attr_prefixes=list_of_valid_wildcard_attr_prefixes, + events=events, + required_args=required_args, + argtext=argtext) + +# This is an initial attempt at resolving type inconsistencies +# between R and JSON. +def json_to_r_type(current_prop): + object_type = current_prop['type'].values() + if 'defaultValue' in current_prop and object_type == ['string']: + if current_prop['defaultValue']['value'].__contains__('\''): + argument = current_prop['defaultValue']['value'] + else: + argument = "\'{:s}\'".format(current_prop['defaultValue']['value']) + elif 'defaultValue' in current_prop and object_type == ['object']: + argument = 'list()' + elif 'defaultValue' in current_prop and \ + current_prop['defaultValue']['value'] == '[]': + argument = 'list()' + else: + argument = 'NULL' + return argument + +# pylint: disable=R0914 +def generate_class_string_r(typename, props, namespace, prefix): + """ + Dynamically generate class strings to have nicely formatted documentation, + and function arguments + + Inspired by http://jameso.be/2013/08/06/namedtuple.html + + Parameters + ---------- + typename + props + namespace + prefix + + Returns + ------- + string + + """ + + c = '''{prefix}{typename} <- function(..., {default_argtext}) {{ + component <- list( + props = list( + {default_paramtext} + ), + type = '{typename}', + namespace = '{namespace}', + propNames = c({prop_names}), + package = '{package_name}' + ) + + component$props <- filter_null(component$props) + component <- append_wildcard_props(component, wildcards = {default_wildcards}, ...) + + structure(component, class = c('dash_component', 'list')) + }} +''' + + # Here we convert from snake case to camel case + package_name = make_package_name_r(namespace) + + prop_keys = list(props.keys()) + + default_paramtext = '' + default_argtext = '' + default_wildcards = '' + + # Produce a string with all property names other than WCs + prop_names = ", ".join( + ('\'{:s}\''.format(p)) + for p in prop_keys + if '*' not in p and + p not in ['setProps', 'dashEvents', 'fireEvent'] + ) + + # in R, we set parameters with no defaults to NULL + # Here we'll do that if no default value exists + default_wildcards += ", ".join( + ('\'{:s}\''.format(p)) + for p in prop_keys + if '*' in p + ) + + if default_wildcards == '': + default_wildcards = 'NULL' + else: + default_wildcards = 'c({:s})'.format(default_wildcards) + + # ('{:s}={}'.format(p, props[p]['defaultValue']['value']) + default_argtext += ", ".join( + ('{:s}={}'.format(p, json_to_r_type(props[p])) + if 'defaultValue' in props[p] else + '{:s}=NULL'.format(p)) + for p in prop_keys + if not p.endswith("-*") and + p not in r_keywords and + p not in ['setProps', 'dashEvents', 'fireEvent'] + ) + + if 'children' in props: + prop_keys.remove('children') + + # pylint: disable=C0301 + default_paramtext += ", ".join( + ('{:s}={:s}'.format(p, p) + if p != "children" else + '{:s}=c(children, assert_valid_children(..., wildcards = {:s}))'.format(p, default_wildcards)) + for p in props.keys() + if not p.endswith("-*") and + p not in r_keywords and + p not in ['setProps', 'dashEvents', 'fireEvent'] + ) + return c.format(prefix=prefix, + typename=typename, + default_argtext=default_argtext, + default_paramtext=default_paramtext, + namespace=namespace, + prop_names=prop_names, + package_name=package_name, + default_wildcards=default_wildcards) # pylint: disable=unused-argument def generate_class_file(typename, props, description, namespace): @@ -466,6 +594,232 @@ def generate_class_file(typename, props, description, namespace): f.write(import_string) f.write(class_string) +def generate_help_file_r(typename, props, prefix): + """ + Generate a R documentation file (.Rd) given component name and properties + + Parameters + ---------- + typename + props + prefix + + Returns + ------- + + + """ + file_name = '{:s}{:s}.Rd'.format(prefix, typename) + prop_keys = list(props.keys()) + + default_argtext = '' + item_text = '' + + # Ensure props are ordered with children first + props = reorder_props(props=props) + + default_argtext += ", ".join( + ('{:s}={}'.format(p, json_to_r_type(props[p])) + if 'defaultValue' in props[p] else + '{:s}=NULL'.format(p)) + for p in prop_keys + if not p.endswith("-*") and + p not in r_keywords and + p not in ['setProps', 'dashEvents', 'fireEvent'] + ) + + item_text += "\n\n".join( + ('\\item{{{:s}}}{{{:s}}}'.format(p, props[p]['description'])) + for p in prop_keys + if not p.endswith("-*") and + p not in r_keywords and + p not in ['setProps', 'dashEvents', 'fireEvent'] + ) + + help_string = '''% Auto-generated: do not edit by hand +\\name{{{prefix}{typename}}} +\\alias{{{prefix}{typename}}} +\\title{{{typename} component}} +\\usage{{ +{prefix}{typename}(..., {default_argtext}) +}} +\\arguments{{ +{item_text} +}} +\\description{{ +{typename} component +}} + ''' + if not os.path.exists('man'): + os.makedirs('man') + + file_path = os.path.join('man', file_name) + with open(file_path, 'w') as f: + f.write(help_string.format( + prefix=prefix, + typename=typename, + default_argtext=default_argtext, + item_text=item_text + )) + +def generate_class_file_r(typename, props, description, namespace, prefix): + """ + Generate a R class file (.R) given a class string + + Parameters + ---------- + typename + props + description + namespace + prefix + + Returns + ------- + + """ + import_string =\ + "# AUTO GENERATED FILE - DO NOT EDIT\n\n" + class_string = generate_class_string_r( + typename, + props, + namespace, + prefix + ) + file_name = "{:s}{:s}.R".format(prefix, typename) + + if not os.path.exists('R'): + os.makedirs('R') + + file_path = os.path.join('R', file_name) + with open(file_path, 'w') as f: + f.write(import_string) + f.write(class_string) + +# pylint: disable=unused-variable +def generate_export_string_r(name, prefix): + if not name.endswith('-*') and \ + str(name) not in r_keywords and \ + str(name) not in ['setProps', 'children', 'dashEvents']: + return 'export({:s}{:s})\n'.format(prefix, name) + +# pylint: disable=R0914 +def generate_rpkg(pkg_data, + namespace, + export_string): + ''' + Generate documents for R package creation + + Parameters + ---------- + name + pkg_data + namespace + + Returns + ------- + + ''' + # Leverage package.json to import specifics which are also applicable + # to R package that we're generating here + package_name = make_package_name_r(namespace) + package_description = pkg_data['description'] + package_version = pkg_data['version'] + package_issues = pkg_data['bugs']['url'] + package_url = pkg_data['homepage'] + + package_author = pkg_data['author'] + + # The following approach avoids use of regex, but there are probably better ways! + package_author_no_email = package_author.split(" <")[0] + ' [aut]' + + if not (os.path.isfile('LICENSE') or os.path.isfile('LICENSE.txt')): + package_license = pkg_data['license'] + else: + package_license = pkg_data['license'] + ' + file LICENSE' + # R requires that the LICENSE.txt file be named LICENSE + if not os.path.isfile('LICENSE'): + os.symlink("LICENSE.txt", "LICENSE") + + import_string =\ + '# AUTO GENERATED FILE - DO NOT EDIT\n\n' + + temp_string =\ + '''export(filter_null) +export(assert_valid_children) +export(append_wildcard_props) +export(names2) +export(`%||%`) +export(assert_no_names) +''' + + description_string = \ + '''Package: {package_name} +Title: {package_description} +Version: {package_version} +Authors @R: as.person(c({package_author})) +Description: {package_description} +Suggests: testthat, roxygen2 +License: {package_license} +URL: {package_url} +BugReports: {package_issues} +Encoding: UTF-8 +LazyData: true +Author: {package_author_no_email} +Maintainer: {package_author} +''' + + description_string = description_string.format(package_name=package_name, + package_description=package_description, + package_version=package_version, + package_author=package_author, + package_license=package_license, + package_url=package_url, + package_issues=package_issues, + package_author_no_email=package_author_no_email) + + rbuild_ignore_string = r'''# ignore JS config files/folders +node_modules/ +coverage/ +src/ +lib/ +.babelrc +.builderrc +.eslintrc +.npmignore + +# demo folder has special meaning in R +# this should hopefully make it still +# allow for the possibility to make R demos +demo/*.js +demo/*.html +demo/*.css + +# ignore python files/folders +setup.py +usage.py +setup.py +requirements.txt +MANIFEST.in +CHANGELOG.md +test/ +# CRAN has weird LICENSE requirements +LICENSE.txt +^.*\.Rproj$ +^\.Rproj\.user$ +''' + + with open('NAMESPACE', 'w') as f: + #f.write(import_string) + #f.write(export_string) + #f.write(temp_string) + f.write('exportPattern("^[^\\\\.]")') + + with open('DESCRIPTION', 'w') as f2: + f2.write(description_string) + + with open('.Rbuildignore', 'w') as f3: + f3.write(rbuild_ignore_string) # pylint: disable=unused-argument def generate_class(typename, props, description, namespace): @@ -882,3 +1236,10 @@ 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 '' + +# This converts a string from snake case to camel case +# Not required for R package name to be in camel case, +# but probably more conventional this way +def make_package_name_r(namestring): + first, rest = namestring.split('_')[0], namestring.split('_')[1:] + return first + ''.join(word.capitalize() for word in rest) From 2a4ac5a490842547733c77a89f03a024061bc4d1 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Fri, 23 Nov 2018 14:36:27 -0500 Subject: [PATCH 18/25] Changes to support transpiling of JSON>>R within Python --- dash/development/component_loader.py | 76 +++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 2b5e70b10f..44755be1b0 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -4,7 +4,10 @@ from .base_component import generate_class from .base_component import generate_class_file from .base_component import ComponentRegistry - +from .base_component import generate_class_file_r +from .base_component import generate_help_file_r +from .base_component import generate_export_string_r +from .base_component import generate_rpkg def _get_metadata(metadata_path): # Start processing @@ -110,3 +113,74 @@ def generate_classes(namespace, metadata_path='lib/metadata.json'): array_string += ' "{:s}",\n'.format(a) array_string += ']\n' f.write('\n\n__all__ = {:s}'.format(array_string)) + +def generate_classes_r(namespace, metadata_path='lib/metadata.json', pkgjson_path='package.json'): + """Load React component metadata into a format Dash can parse, + then create python class files. + + Usage: generate_classes_r() + + 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). + + pkgjson_path -- a path to a JSON file created by + [`cookiecutter`](https://github.com/audreyr/cookiecutter). + + Returns: + """ + + data = _get_metadata(metadata_path) + pkg_data = _get_metadata(pkgjson_path) + imports_path = os.path.join(namespace, '_imports_.py') + export_string = '' + + if namespace == 'dash_html_components': + prefix = 'html' + elif namespace == 'dash_core_components': + prefix = 'core' + else: + prefix = '' + + # Make sure the file doesn't exist, as we use append to write lines + if os.path.exists(imports_path): + os.remove(imports_path) + + # Remove the R NAMESPACE file if it exists, this will be repopulated + if os.path.isfile('NAMESPACE'): + os.remove('NAMESPACE') + + # 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] + + export_string += generate_export_string_r(name, prefix) + + generate_class_file_r( + name, + componentData['props'], + componentData['description'], + namespace, + prefix + ) + + generate_help_file_r( + name, + componentData['props'], + prefix + ) + + generate_rpkg( + pkg_data, + namespace, + export_string + ) From 3341d8e220d84a99c3b3aa44f2f69643332d15fd Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Fri, 23 Nov 2018 14:55:39 -0500 Subject: [PATCH 19/25] Restore export_string generation --- dash/development/base_component.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 036cba99c9..ed2d7fe8da 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -528,7 +528,6 @@ def generate_class_string_r(typename, props, namespace, prefix): else: default_wildcards = 'c({:s})'.format(default_wildcards) - # ('{:s}={}'.format(p, props[p]['defaultValue']['value']) default_argtext += ", ".join( ('{:s}={}'.format(p, json_to_r_type(props[p])) if 'defaultValue' in props[p] else @@ -730,7 +729,6 @@ def generate_rpkg(pkg_data, package_author = pkg_data['author'] - # The following approach avoids use of regex, but there are probably better ways! package_author_no_email = package_author.split(" <")[0] + ' [aut]' if not (os.path.isfile('LICENSE') or os.path.isfile('LICENSE.txt')): @@ -744,15 +742,6 @@ def generate_rpkg(pkg_data, import_string =\ '# AUTO GENERATED FILE - DO NOT EDIT\n\n' - temp_string =\ - '''export(filter_null) -export(assert_valid_children) -export(append_wildcard_props) -export(names2) -export(`%||%`) -export(assert_no_names) -''' - description_string = \ '''Package: {package_name} Title: {package_description} @@ -810,10 +799,8 @@ def generate_rpkg(pkg_data, ''' with open('NAMESPACE', 'w') as f: - #f.write(import_string) - #f.write(export_string) - #f.write(temp_string) - f.write('exportPattern("^[^\\\\.]")') + f.write(import_string) + f.write(export_string) with open('DESCRIPTION', 'w') as f2: f2.write(description_string) From 0d7943dba3b2c01c3ab4912fc5b980ba57687ead Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Tue, 27 Nov 2018 11:20:28 -0500 Subject: [PATCH 20/25] Updates to component loader/base component --- dash/development/base_component.py | 109 ++++++++++++++++++++++++++- dash/development/component_loader.py | 44 ++++++++--- 2 files changed, 138 insertions(+), 15 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index ed2d7fe8da..e34fbe4261 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -560,6 +560,83 @@ def generate_class_string_r(typename, props, namespace, prefix): package_name=package_name, default_wildcards=default_wildcards) +# pylint: disable=R0914 +def generate_js_metadata_r(namespace): + """ + Dynamically generate R function to supply JavaScript + dependency information required by htmlDependency package, + which is loaded by dashR. + + Inspired by http://jameso.be/2013/08/06/namedtuple.html + + Parameters + ---------- + namespace + + Returns + ------- + function_string + """ + + project_shortname = namespace.replace('-', '_') + + import importlib + + # import component library module + importlib.import_module(project_shortname) + + # import component library module into sys + mod = sys.module[project_shortname] + + jsdist = getattr(mod, '_js_dist', []) + project_ver = getattr(mod, '__version__', []) + + rpkgname = make_package_name_r(project_shortname) + + jsbundle_url = jsdist.external_url() + + # because a library may have more than one dependency, need + # a way to iterate over all dependencies for a given library + # here we define an opening, element, and closing string -- + # if the total number of dependencies > 1, we can string each + # together and write a list object in R with multiple elements + function_frame_open = '''.{rpkgname}_js_metadata <- function() { + deps_metadata <- list( + ''' + + function_frame_element = '''`{project_shortname}` = structure(list(name = "{project_shortname}", + version = "{project_ver}", src = list(href = "{jsbundle_url}", + file = "lib/{project_shortname}@{project_ver}"), meta = NULL, + script = "{project_shortname}/{project_shortname}.min.js", + stylesheet = NULL, head = NULL, attachment = NULL, package = "{rpkgname}", + all_files = FALSE), class = "html_dependency") + ''' + + function_frame_close = ''') + return(deps_metadata) + }} + ''' + + function_frame_body = '' + + function_frame_body = ",\n".join( + (function_frame_element.format(rpkgname=rpkgname, + project_shortname=project_shortname, + project_ver=project_ver, + jsbundle_url=jsbundle_url) + if 'defaultValue' in props[p] else + '{:s}=NULL'.format(p)) + for p in prop_keys + if not p.endswith("-*") and + p not in r_keywords and + p not in ['setProps', 'dashEvents', 'fireEvent'] + ) + + return function_string.format(rpkgname=rpkgname, + project_shortname=project_shortname, + project_ver=project_ver, + jsbundle_url=jsbundle_url) + # pylint: disable=unused-argument def generate_class_file(typename, props, description, namespace): """ @@ -593,9 +670,9 @@ def generate_class_file(typename, props, description, namespace): f.write(import_string) f.write(class_string) -def generate_help_file_r(typename, props, prefix): +def write_help_file_r(typename, props, prefix): """ - Generate a R documentation file (.Rd) given component name and properties + Write R documentation file (.Rd) given component name and properties Parameters ---------- @@ -661,7 +738,7 @@ def generate_help_file_r(typename, props, prefix): item_text=item_text )) -def generate_class_file_r(typename, props, description, namespace, prefix): +def write_class_file_r(typename, props, description, namespace, prefix): """ Generate a R class file (.R) given a class string @@ -702,6 +779,32 @@ def generate_export_string_r(name, prefix): str(name) not in ['setProps', 'children', 'dashEvents']: return 'export({:s}{:s})\n'.format(prefix, name) +def write_js_metadata_r(namespace): + """ + Write an internal (not exported) function to return all JS + dependencies as required by htmlDependency package given a + function string + + Parameters + ---------- + namespace + + Returns + ------- + + """ + function_string = generate_js_metadata_r( + namespace + ) + file_name = "internal.R" + + if not os.path.exists('R'): + os.makedirs('R') + + file_path = os.path.join('R', file_name) + with open(file_path, 'w') as f: + f.write(function_string) + # pylint: disable=R0914 def generate_rpkg(pkg_data, namespace, diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 44755be1b0..387e6ed398 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -1,14 +1,18 @@ import collections import json import os + +from .base_component import ComponentRegistry + from .base_component import generate_class from .base_component import generate_class_file -from .base_component import ComponentRegistry -from .base_component import generate_class_file_r -from .base_component import generate_help_file_r from .base_component import generate_export_string_r from .base_component import generate_rpkg +from .base_component import write_class_file_r +from .base_component import write_help_file_r +from .base_component import write_js_metadata_r + def _get_metadata(metadata_path): # Start processing with open(metadata_path) as data_file: @@ -124,7 +128,7 @@ def generate_classes_r(namespace, metadata_path='lib/metadata.json', pkgjson_pat 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). + [`reac-docgen`](https://github.com/reactjs/react-docgen). pkgjson_path -- a path to a JSON file created by [`cookiecutter`](https://github.com/audreyr/cookiecutter). @@ -144,10 +148,6 @@ def generate_classes_r(namespace, metadata_path='lib/metadata.json', pkgjson_pat else: prefix = '' - # Make sure the file doesn't exist, as we use append to write lines - if os.path.exists(imports_path): - os.remove(imports_path) - # Remove the R NAMESPACE file if it exists, this will be repopulated if os.path.isfile('NAMESPACE'): os.remove('NAMESPACE') @@ -164,21 +164,41 @@ def generate_classes_r(namespace, metadata_path='lib/metadata.json', pkgjson_pat name = componentPath.split('/').pop().split('.')[0] export_string += generate_export_string_r(name, prefix) - - generate_class_file_r( + + # generate and write out R functions which will serve an analogous + # purpose to the classes in Python which interface with the + # Dash components + write_class_file_r( name, componentData['props'], componentData['description'], namespace, prefix ) - - generate_help_file_r( + + # generate the internal (not exported to the user) functions which + # supply the JavaScript dependencies to the htmlDependency package, + # which is required by DashR (this avoids having to generate an + # RData file from within Python, given the current package generation + # workflow) + write_js_metadata_r( + namespace + ) + + # generate the R help pages for each of the Dash components that we + # are transpiling -- this is done to avoid using Roxygen2 syntax, + # we may eventually be able to generate similar documentation using + # doxygen and an R plugin, but for now we'll just do it on our own + # from within Python + write_help_file_r( name, componentData['props'], prefix ) + # now, bundle up the package information and create all the requisite + # elements of an R package, so that the end result is installable either + # locally or directly from GitHub generate_rpkg( pkg_data, namespace, From 13d0d7158bb1ff0e62164175c51f11f98592c3e0 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Tue, 27 Nov 2018 16:42:42 -0500 Subject: [PATCH 21/25] Updates to generate_js_metadata_r --- dash/development/base_component.py | 81 ++++++++++++++++++------------ 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 64d3fdbb40..54f6212c35 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -563,7 +563,7 @@ def generate_class_string_r(typename, props, namespace, prefix): def generate_js_metadata_r(namespace): """ Dynamically generate R function to supply JavaScript - dependency information required by htmlDependency package, + dependency information required by htmltools package, which is loaded by dashR. Inspired by http://jameso.be/2013/08/06/namedtuple.html @@ -585,56 +585,73 @@ def generate_js_metadata_r(namespace): importlib.import_module(project_shortname) # import component library module into sys - mod = sys.module[project_shortname] + mod = sys.modules[project_shortname] jsdist = getattr(mod, '_js_dist', []) project_ver = getattr(mod, '__version__', []) rpkgname = make_package_name_r(project_shortname) - jsbundle_url = jsdist.external_url() - # because a library may have more than one dependency, need # a way to iterate over all dependencies for a given library # here we define an opening, element, and closing string -- # if the total number of dependencies > 1, we can string each # together and write a list object in R with multiple elements - function_frame_open = '''.{rpkgname}_js_metadata <- function() { - deps_metadata <- list( - ''' - function_frame_element = '''`{project_shortname}` = structure(list(name = "{project_shortname}", - version = "{project_ver}", src = list(href = "{jsbundle_url}", - file = "lib/{project_shortname}@{project_ver}"), meta = NULL, - script = "{project_shortname}/{project_shortname}.min.js", - stylesheet = NULL, head = NULL, attachment = NULL, package = "{rpkgname}", - all_files = FALSE), class = "html_dependency") - ''' + function_frame_open = '''.{rpkgname}_js_metadata <- function() {{ + deps_metadata <- list( + '''.format(rpkgname=rpkgname) + + function_frame = [] + + # the following string represents all the elements in an object + # of the html_dependency class, which will be propagated by + # iterating over __init__.py + function_frame_element = '''`{dep_name}` = structure(list(name = "{dep_name}", + version = "{project_ver}", src = list(href = "{jsbundle_url}", + file = "lib/{dep_name}@{project_ver}"), meta = NULL, + script = "{project_shortname}/{dep_rpp}", + stylesheet = NULL, head = NULL, attachment = NULL, package = "{rpkgname}", + all_files = FALSE), class = "html_dependency")''' + + if len(jsdist) > 1: + for dep in range(len(jsdist)): + if jsdist[dep]['relative_package_path'].__contains__('dash-'): + dep_name = jsdist[dep]['relative_package_path'].split('.')[0] + else: + dep_name = '{:s}_{:s}'.format(project_shortname, str(dep)) + project_ver = str(dep) + function_frame += [function_frame_element.format(dep_name=dep_name, + project_ver=project_ver, + rpkgname=rpkgname, + project_shortname=project_shortname, + dep_rpp=jsdist[dep]['relative_package_path'], + jsbundle_url=jsdist[dep]['external_url']) + ] + function_frame_body = ',\n'.join(function_frame) + elif len(jsdist) == 1: + dep_name = project_shortname + dep_rpp = jsdist[0]['relative_package_path'] + jsbundle_url = jsdist[0]['external_url'] + + function_frame_body = ['''`{project_shortname}` = structure(list(name = "{project_shortname}", + version = "{project_ver}", src = list(href = "{jsbundle_url}", + file = "lib/{project_shortname}@{project_ver}"), meta = NULL, + script = "{project_shortname}/{project_shortname}.min.js", + stylesheet = NULL, head = NULL, attachment = NULL, package = "{rpkgname}", + all_files = FALSE), class = "html_dependency")'''] function_frame_close = ''') return(deps_metadata) }} ''' - function_frame_body = '' - - function_frame_body = ",\n".join( - (function_frame_element.format(rpkgname=rpkgname, - project_shortname=project_shortname, - project_ver=project_ver, - jsbundle_url=jsbundle_url) - if 'defaultValue' in props[p] else - '{:s}=NULL'.format(p)) - for p in prop_keys - if not p.endswith("-*") and - p not in r_keywords and - p not in ['setProps', 'dashEvents', 'fireEvent'] - ) + function_string = ''.join([function_frame_open, + function_frame_body, + function_frame_close] + ) - return function_string.format(rpkgname=rpkgname, - project_shortname=project_shortname, - project_ver=project_ver, - jsbundle_url=jsbundle_url) + return function_string # pylint: disable=unused-argument def generate_class_file(typename, props, description, namespace): From 8a43d98cd8fa91fd8ca1beeb20996e0e71b8e562 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Wed, 28 Nov 2018 12:25:46 -0500 Subject: [PATCH 22/25] Removed external_url parsing --- dash/development/base_component.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 54f6212c35..4c71a23f88 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -592,8 +592,8 @@ def generate_js_metadata_r(namespace): rpkgname = make_package_name_r(project_shortname) - # because a library may have more than one dependency, need - # a way to iterate over all dependencies for a given library + # since _js_dist may suggest more than one dependency, need + # a way to iterate over all dependencies for a given set. # here we define an opening, element, and closing string -- # if the total number of dependencies > 1, we can string each # together and write a list object in R with multiple elements @@ -608,9 +608,9 @@ def generate_js_metadata_r(namespace): # of the html_dependency class, which will be propagated by # iterating over __init__.py function_frame_element = '''`{dep_name}` = structure(list(name = "{dep_name}", - version = "{project_ver}", src = list(href = "{jsbundle_url}", - file = "lib/{dep_name}@{project_ver}"), meta = NULL, - script = "{project_shortname}/{dep_rpp}", + version = "{project_ver}", src = list(href = NULL, + file = "lib/"), meta = NULL, + script = "{dep_rpp}", stylesheet = NULL, head = NULL, attachment = NULL, package = "{rpkgname}", all_files = FALSE), class = "html_dependency")''' @@ -625,8 +625,7 @@ def generate_js_metadata_r(namespace): project_ver=project_ver, rpkgname=rpkgname, project_shortname=project_shortname, - dep_rpp=jsdist[dep]['relative_package_path'], - jsbundle_url=jsdist[dep]['external_url']) + dep_rpp=jsdist[dep]['relative_package_path']) ] function_frame_body = ',\n'.join(function_frame) elif len(jsdist) == 1: @@ -635,16 +634,15 @@ def generate_js_metadata_r(namespace): jsbundle_url = jsdist[0]['external_url'] function_frame_body = ['''`{project_shortname}` = structure(list(name = "{project_shortname}", - version = "{project_ver}", src = list(href = "{jsbundle_url}", - file = "lib/{project_shortname}@{project_ver}"), meta = NULL, - script = "{project_shortname}/{project_shortname}.min.js", + version = "{project_ver}", src = list(href = NULL, + file = "lib/"), meta = NULL, + script = "{project_shortname}.min.js", stylesheet = NULL, head = NULL, attachment = NULL, package = "{rpkgname}", all_files = FALSE), class = "html_dependency")'''] function_frame_close = ''') return(deps_metadata) - }} - ''' + }''' function_string = ''.join([function_frame_open, function_frame_body, From 8d2d5388ff2ffe8da57604788ee29bf0e4a4c75b Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Wed, 28 Nov 2018 12:28:18 -0500 Subject: [PATCH 23/25] Clarified comments in code --- dash/development/base_component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 4c71a23f88..d61dba76ad 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -606,7 +606,7 @@ def generate_js_metadata_r(namespace): # the following string represents all the elements in an object # of the html_dependency class, which will be propagated by - # iterating over __init__.py + # iterating over _js_dist in __init__.py function_frame_element = '''`{dep_name}` = structure(list(name = "{dep_name}", version = "{project_ver}", src = list(href = NULL, file = "lib/"), meta = NULL, From 008598f0987855ea4606558f42db36574a3d745d Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Wed, 28 Nov 2018 13:07:55 -0500 Subject: [PATCH 24/25] Added statements to copy JS deps into inst/lib --- dash/development/base_component.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index d61dba76ad..1a5101c2e3 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -819,6 +819,13 @@ def write_js_metadata_r(namespace): with open(file_path, 'w') as f: f.write(function_string) + # now copy over all the JS dependencies from the (Python) + # components directory + import shutil, glob + + for file in glob.glob('{}/*.js'.format(namespace.replace('-', '_'))): + shutil.copy(file, 'inst/lib/') + # pylint: disable=R0914 def generate_rpkg(pkg_data, namespace, From 918a07350409b21aa287b6deed086641f458c397 Mon Sep 17 00:00:00 2001 From: Ryan Patrick Kyle Date: Wed, 28 Nov 2018 18:26:02 -0500 Subject: [PATCH 25/25] Updated base_component.py --- dash/development/base_component.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 1a5101c2e3..e98dd2b86d 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -616,7 +616,7 @@ def generate_js_metadata_r(namespace): if len(jsdist) > 1: for dep in range(len(jsdist)): - if jsdist[dep]['relative_package_path'].__contains__('dash-'): + if jsdist[dep]['relative_package_path'].__contains__('dash_'): dep_name = jsdist[dep]['relative_package_path'].split('.')[0] else: dep_name = '{:s}_{:s}'.format(project_shortname, str(dep)) @@ -629,16 +629,15 @@ def generate_js_metadata_r(namespace): ] function_frame_body = ',\n'.join(function_frame) elif len(jsdist) == 1: - dep_name = project_shortname - dep_rpp = jsdist[0]['relative_package_path'] - jsbundle_url = jsdist[0]['external_url'] - - function_frame_body = ['''`{project_shortname}` = structure(list(name = "{project_shortname}", + function_frame_body = '''`{project_shortname}` = structure(list(name = "{project_shortname}", version = "{project_ver}", src = list(href = NULL, file = "lib/"), meta = NULL, - script = "{project_shortname}.min.js", + script = "{dep_rpp}", stylesheet = NULL, head = NULL, attachment = NULL, package = "{rpkgname}", - all_files = FALSE), class = "html_dependency")'''] + all_files = FALSE), class = "html_dependency")'''.format(project_shortname=project_shortname, + project_ver=project_ver, + rpkgname=rpkgname, + dep_rpp=jsdist[0]['relative_package_path']) function_frame_close = ''') return(deps_metadata)