diff --git a/dash/development/_r_components_generation.py b/dash/development/_r_components_generation.py index e26b1740a9..47058b0af5 100644 --- a/dash/development/_r_components_generation.py +++ b/dash/development/_r_components_generation.py @@ -8,6 +8,7 @@ import importlib import textwrap import re +import warnings from ._all_keywords import r_keywords from ._py_components_generation import reorder_props @@ -18,16 +19,18 @@ # code below r_component_string = """{funcname} <- function({default_argtext}{wildcards}) {{ {wildcard_declaration} + props <- list({default_paramtext}{wildcards}) + if (length(props) > 0) {{ + props <- props[!vapply(props, is.null, logical(1))] + }} component <- list( - props = list({default_paramtext}{wildcards}), + props = props, type = '{name}', namespace = '{project_shortname}', propNames = c({prop_names}{wildcard_names}), package = '{package_name}' ) - component$props <- filter_null(component$props) - structure(component, class = c('dash_component', 'list')) }} """ # noqa:E501 @@ -83,7 +86,7 @@ Authors @R: as.person(c({package_author})) Description: {package_description} Depends: R (>= 3.0.2){package_depends} -Imports: dash{package_imports} +Imports: {package_imports} Suggests: {package_suggests} License: {package_license} URL: {package_url} @@ -91,7 +94,7 @@ Encoding: UTF-8 LazyData: true Author: {package_author_no_email} -Maintainer: {package_author} +Maintainer: {maintainer} """ rbuild_ignore_string = r"""# ignore JS config files/folders @@ -103,6 +106,11 @@ .builderrc .eslintrc .npmignore +.editorconfig +.eslintignore +.prettierrc +.circleci +.github # demo folder has special meaning in R # this should hopefully make it still @@ -129,22 +137,44 @@ \\docType{{package}} \\name{{{package_name}-package}} \\alias{{{package_name}}} -\\title{{{pkg_help_header}}} +\\title{{{pkg_help_title}}} \\description{{ -{pkg_help_desc} -}} -\\seealso{{ -Useful links: -\\itemize{{ - \\item \\url{{https://github.com/plotly/{lib_name}}} - \\item Report bugs at \\url{{https://github.com/plotly/{lib_name}/issues}} -}} +{pkg_help_description} }} \\author{{ -\\strong{{Maintainer}}: {package_author} +\\strong{{Maintainer}}: {maintainer} }} """ +wildcard_helper = """ +dash_assert_valid_wildcards <- function (attrib = list("data", "aria"), ...) +{ + args <- list(...) + validation_results <- lapply(names(args), function(x) { + grepl(paste0("^(", paste0(attrib, collapse="|"), ")-[a-zA-Z0-9_-]+$"), + x) + }) + if (FALSE %in% validation_results) { + stop(sprintf("The following props are not valid in this component: '%s'", + paste(names(args)[grepl(FALSE, unlist(validation_results))], + collapse = ", ")), call. = FALSE) + } + else { + return(args) + } +} +""" # noqa:E501 + +wildcard_template = """ + wildcard_names = names(dash_assert_valid_wildcards(attrib = list({}), ...)) +""" + +wildcard_help_template = """ + + +\\item{{...}}{{wildcards allowed have the form: `{}`}} +""" + # pylint: disable=R0914 def generate_class_string(name, props, project_shortname, prefix): @@ -159,18 +189,18 @@ def generate_class_string(name, props, project_shortname, prefix): wildcards = "" wildcard_declaration = "" wildcard_names = "" + default_paramtext = "" + default_argtext = "" + accepted_wildcards = "" - if any("-*" in key for key in prop_keys): + if any(key.endswith("-*") for key in prop_keys): + accepted_wildcards = get_wildcards_r(prop_keys) wildcards = ", ..." - wildcard_declaration = ( - "\n wildcard_names = names(assert_valid_wildcards(...))\n" + wildcard_declaration = wildcard_template.format( + accepted_wildcards.replace("-*", "") ) wildcard_names = ", wildcard_names" - default_paramtext = "" - default_argtext = "" - default_wildcards = "" - # Produce a string with all property names other than WCs prop_names = ", ".join( "'{}'".format(p) @@ -178,20 +208,16 @@ def generate_class_string(name, props, project_shortname, prefix): if "*" not in p and p not in ["setProps"] ) - # in R, we set parameters with no defaults to NULL - # Here we'll do that if no default value exists - default_wildcards += ", ".join("'{}'".format(p) - for p in prop_keys if "*" in p) - - if default_wildcards == "": - default_wildcards = "NULL" - else: - default_wildcards = "c({})".format(default_wildcards) - # Filter props to remove those we don't want to expose for item in prop_keys[:]: - if item.endswith("-*") or item in r_keywords or item == "setProps": + if item.endswith("-*") or item == "setProps": prop_keys.remove(item) + elif item in r_keywords: + prop_keys.remove(item) + warnings.warn(( + 'WARNING: prop "{}" in component "{}" is an R keyword' + ' - REMOVED FROM THE R COMPONENT' + ).format(item, name)) default_argtext += ", ".join("{}=NULL".format(p) for p in prop_keys) @@ -256,7 +282,7 @@ def generate_js_metadata(pkg_data, project_shortname): if "dash_" in rpp: dep_name = rpp.split(".")[0] else: - dep_name = "{}_{}".format(project_shortname, str(dep)) + dep_name = "{}".format(project_shortname) project_ver = str(dep) if "css" in rpp: css_name = "'{}'".format(rpp) @@ -278,10 +304,10 @@ def generate_js_metadata(pkg_data, project_shortname): elif len(alldist) == 1: rpp = alldist[0]["relative_package_path"] if "css" in rpp: - css_name = rpp + css_name = "'{}'".format(rpp) script_name = "NULL" else: - script_name = rpp + script_name = "'{}'".format(rpp) css_name = "NULL" function_frame_body = frame_body_template.format( project_shortname=project_shortname, @@ -321,8 +347,6 @@ def write_help_file(name, props, description, prefix): prop_keys = list(props.keys()) - has_wildcards = any("-*" in key for key in prop_keys) - # Filter props to remove those we don't want to expose for item in prop_keys[:]: if item.endswith("-*") or item in r_keywords or item == "setProps": @@ -331,29 +355,28 @@ def write_help_file(name, props, description, prefix): default_argtext += ", ".join("{}=NULL".format(p) for p in prop_keys) item_text += "\n\n".join( - "\\item{{{}}}{{{}{}}}".format(p, - print_r_type( - props[p]["type"] - ), - props[p]["description"]) + "\\item{{{}}}{{{}{}}}".format( + p, + print_r_type(props[p]["type"]), + props[p]["description"] + ) for p in prop_keys ) - if has_wildcards: - item_text += '\n\n\\item{...}{wildcards: `data-*` or `aria-*`}' + if any(key.endswith("-*") for key in prop_keys): default_argtext += ', ...' + item_text += wildcard_help_template.format(get_wildcards_r(prop_keys)) # in R, the online help viewer does not properly wrap lines for # the usage string -- we will hard wrap at 80 characters using # textwrap.fill, starting from the beginning of the usage string - argtext = prefix + name + "({})".format(default_argtext) file_path = os.path.join('man', file_name) with open(file_path, 'w') as f: f.write(help_string.format( funcname=format_fn_name(prefix, name), name=name, - default_argtext=textwrap.fill(argtext, + default_argtext=textwrap.fill(default_argtext, width=80, break_long_words=False), item_text=item_text, @@ -361,11 +384,7 @@ def write_help_file(name, props, description, prefix): )) -def write_class_file(name, - props, - description, - project_shortname, - prefix=None): +def write_class_file(name, props, description, project_shortname, prefix=None): props = reorder_props(props=props) # generate the R help pages for each of the Dash components that we @@ -373,20 +392,12 @@ def write_class_file(name, # 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( - name, - props, - description, - prefix - ) + write_help_file(name, props, description, prefix) import_string =\ "# AUTO GENERATED FILE - DO NOT EDIT\n\n" class_string = generate_class_string( - name, - props, - project_shortname, - prefix + name, props, project_shortname, prefix ) file_name = format_fn_name(prefix, name) + ".R" @@ -399,7 +410,7 @@ def write_class_file(name, print("Generated {}".format(file_name)) -def write_js_metadata(pkg_data, project_shortname): +def write_js_metadata(pkg_data, project_shortname, has_wildcards): """ Write an internal (not exported) R function to return all JS dependencies as required by dash. @@ -425,6 +436,8 @@ def write_js_metadata(pkg_data, project_shortname): file_path = os.path.join("R", file_name) with open(file_path, "w") as f: f.write(function_string) + if has_wildcards: + f.write(wildcard_helper) # now copy over all JS dependencies from the (Python) components dir # the inst/lib directory for the package won't exist on first call @@ -445,11 +458,13 @@ def write_js_metadata(pkg_data, project_shortname): # pylint: disable=R0914, R0913, R0912, R0915 def generate_rpkg( pkg_data, + rpkg_data, project_shortname, export_string, package_depends, package_imports, - package_suggests + package_suggests, + has_wildcards, ): """ Generate documents for R package creation @@ -457,8 +472,12 @@ def generate_rpkg( Parameters ---------- pkg_data + rpkg_data project_shortname export_string + package_depends + package_imports + package_suggests Returns ------- @@ -478,12 +497,12 @@ def generate_rpkg( package_depends = ", " + package_depends.strip(",").lstrip() if package_imports: - package_imports = ", " + package_imports.strip(",").lstrip() + package_imports = package_imports.strip(",").lstrip() if package_suggests: package_suggests = package_suggests.strip(",").lstrip() - if "bugs" in pkg_data.keys(): + if "bugs" in pkg_data: package_issues = pkg_data["bugs"].get("url", "") else: package_issues = "" @@ -493,7 +512,7 @@ def generate_rpkg( file=sys.stderr, ) - if "homepage" in pkg_data.keys(): + if "homepage" in pkg_data: package_url = pkg_data.get("homepage", "") else: package_url = "" @@ -506,6 +525,8 @@ def generate_rpkg( package_author_no_email = package_author.split(" <")[0] + " [aut]" + maintainer = pkg_data.get("maintainer", pkg_data.get("author")) + if not (os.path.isfile("LICENSE") or os.path.isfile("LICENSE.txt")): package_license = pkg_data.get("license", "") else: @@ -529,38 +550,16 @@ def generate_rpkg( # generate the internal (not exported to the user) functions which # supply the JavaScript dependencies to the dash package. # this avoids having to generate an RData file from within Python. - write_js_metadata(pkg_data=pkg_data, project_shortname=project_shortname) + write_js_metadata(pkg_data, project_shortname, has_wildcards) - with open("NAMESPACE", "w") as f: + with open("NAMESPACE", "w+") as f: f.write(import_string) f.write(export_string) f.write(packages_string) - with open(".Rbuildignore", "w") as f2: + with open(".Rbuildignore", "w+") as f2: f2.write(rbuild_ignore_string) - # Write package stub files for R online help, generate if - # dashHtmlComponents or dashCoreComponents; makes it easy - # for R users to bring up main package help page - pkg_help_header = "" - - if package_name in ["dashHtmlComponents"]: - pkg_help_header = "Vanilla HTML Components for Dash" - pkg_help_desc = "Dash is a web application framework that\n\ -provides pure Python and R abstraction around HTML, CSS, and\n\ -JavaScript. Instead of writing HTML or using an HTML\n\ -templating engine, you compose your layout using R\n\ -functions within the dashHtmlComponents package. The\n\ -source for this package is on GitHub:\n\ -plotly/dash-html-components." - if package_name in ["dashCoreComponents"]: - pkg_help_header = "Core Interactive UI Components for Dash" - pkg_help_desc = "Dash ships with supercharged components for\n\ -interactive user interfaces. A core set of components,\n\ -written and maintained by the Dash team, is available in\n\ -the dashCoreComponents package. The source for this package\n\ -is on GitHub: plotly/dash-core-components." - description_string = description_template.format( package_name=package_name, package_description=package_description, @@ -573,21 +572,23 @@ def generate_rpkg( package_url=package_url, package_issues=package_issues, package_author_no_email=package_author_no_email, + maintainer=maintainer, ) - with open("DESCRIPTION", "w") as f3: + with open("DESCRIPTION", "w+") as f3: f3.write(description_string) - if pkg_help_header != "": - pkghelp = pkghelp_stub.format( - package_name=package_name, - pkg_help_header=pkg_help_header, - pkg_help_desc=pkg_help_desc, - lib_name=lib_name, - package_author=package_author, - ) - with open(pkghelp_stub_path, "w") as f4: - f4.write(pkghelp) + if rpkg_data is not None: + if rpkg_data.get("pkg_help_description"): + pkghelp = pkghelp_stub.format( + package_name=package_name, + pkg_help_title=rpkg_data.get("pkg_help_title"), + pkg_help_description=rpkg_data.get("pkg_help_description"), + lib_name=lib_name, + maintainer=maintainer, + ) + with open(pkghelp_stub_path, "w") as f4: + f4.write(pkghelp) # This converts a string from snake case to camel case @@ -614,6 +615,7 @@ def generate_exports( components, metadata, pkg_data, + rpkg_data, prefix, package_depends, package_imports, @@ -625,9 +627,7 @@ def generate_exports( if ( not component.endswith("-*") and str(component) not in r_keywords - and str(component) not in ["setProps", - "children", - "dashEvents"] + and str(component) not in ["setProps", "children"] ): export_string += "export({}{})\n".format(prefix, component) @@ -659,16 +659,25 @@ def generate_exports( export_string += "\n".join("export({})".format(function) for function in fnlist) + # Look for wildcards in the metadata + has_wildcards = False + for component_data in metadata.values(): + if any(key.endswith('-*') for key in component_data['props']): + has_wildcards = True + break + # 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, + rpkg_data, project_shortname, export_string, package_depends, package_imports, package_suggests, + has_wildcards, ) @@ -821,3 +830,13 @@ def create_prop_docstring_r(prop_name, type_object, required, description, ': {}'.format(description) if description != '' else '' ), is_required='required' if required else 'optional') + + +def get_wildcards_r(prop_keys): + wildcards = "" + wildcards += ", ".join("'{}'".format(p) + for p in prop_keys if p.endswith("-*")) + + if wildcards == "": + wildcards = "NULL" + return wildcards diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index e1c916a465..9c8abef3d3 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -11,6 +11,7 @@ import functools import pkg_resources +import yaml from ._r_components_generation import write_class_file from ._r_components_generation import generate_exports @@ -50,11 +51,10 @@ def generate_components( project_shortname = project_shortname.replace("-", "_").rstrip("/\\") - if rprefix is not None: - prefix = rprefix - is_windows = sys.platform == "win32" + yamldata = None + extract_path = pkg_resources.resource_filename("dash", "extract-meta.js") reserved_patterns = '|'.join('^{}$'.format(p) for p in reserved_words) @@ -89,12 +89,7 @@ def generate_components( ) sys.exit(1) - jsondata_unicode = json.loads(out.decode(), object_pairs_hook=OrderedDict) - - if sys.version_info[0] >= 3: - metadata = jsondata_unicode - else: - metadata = byteify(jsondata_unicode) + metadata = safe_json_loads(out.decode()) generator_methods = [generate_class_file] @@ -104,7 +99,7 @@ def generate_components( if not os.path.exists('R'): os.makedirs('R') generator_methods.append( - functools.partial(write_class_file, prefix=prefix)) + functools.partial(write_class_file, prefix=rprefix)) components = generate_classes_files( project_shortname, @@ -118,25 +113,39 @@ def generate_components( generate_imports(project_shortname, components) if rprefix is not None: + if os.path.isfile("dash-info.yaml"): + with open("dash-info.yaml") as yamldata: + rpkg_data = yaml.safe_load(yamldata) + else: + rpkg_data = None + print( + "Warning: dash-info.yaml missing; package metadata not loaded", + file=sys.stderr + ) + with open('package.json', 'r') as f: - jsondata_unicode = json.load(f, object_pairs_hook=OrderedDict) - if sys.version_info[0] >= 3: - pkg_data = jsondata_unicode - else: - pkg_data = byteify(jsondata_unicode) + pkg_data = safe_json_loads(f.read()) generate_exports( project_shortname, components, metadata, pkg_data, - prefix, + rpkg_data, + rprefix, rdepends, rimports, rsuggests, ) +def safe_json_loads(s): + jsondata_unicode = json.loads(s, object_pairs_hook=OrderedDict) + if sys.version_info[0] >= 3: + return jsondata_unicode + return byteify(jsondata_unicode) + + def cli(): parser = argparse.ArgumentParser( prog="dash-generate-components", diff --git a/requires-ci.txt b/requires-ci.txt index 7533eca412..fec63ac1ee 100644 --- a/requires-ci.txt +++ b/requires-ci.txt @@ -3,7 +3,8 @@ dash-dangerously-set-inner-html six mock flake8 +PyYAML>=5.1.1 pylint==1.9.4;python_version<"3.7" pylint==2.3.1;python_version=="3.7" astroid==2.2.5;python_version=="3.7" -virtualenv;python_version=="2.7" \ No newline at end of file +virtualenv;python_version=="2.7" diff --git a/requires-install.txt b/requires-install.txt index b5afa621c9..2a2bec1af8 100644 --- a/requires-install.txt +++ b/requires-install.txt @@ -1,4 +1,5 @@ Flask>=1.0.2 +PyYAML>=5.1.1 flask-compress plotly dash_renderer==1.0.0