1006 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1006 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# pylint: disable=consider-using-f-string
 | 
						|
# type: ignore
 | 
						|
import os
 | 
						|
import sys
 | 
						|
import shutil
 | 
						|
import importlib
 | 
						|
import textwrap
 | 
						|
import re
 | 
						|
import warnings
 | 
						|
 | 
						|
from ._all_keywords import r_keywords
 | 
						|
from ._py_components_generation import reorder_props
 | 
						|
 | 
						|
 | 
						|
# Declaring longer string templates as globals to improve
 | 
						|
# readability, make method logic clearer to anyone inspecting
 | 
						|
# code below
 | 
						|
r_component_string = """#' @export
 | 
						|
{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 = props,
 | 
						|
        type = '{name}',
 | 
						|
        namespace = '{project_shortname}',
 | 
						|
        propNames = c({prop_names}{wildcard_names}),
 | 
						|
        package = '{package_name}'
 | 
						|
        )
 | 
						|
 | 
						|
    structure(component, class = c('dash_component', 'list'))
 | 
						|
}}
 | 
						|
"""  # noqa:E501
 | 
						|
 | 
						|
# the following strings represent all the elements in an object
 | 
						|
# of the html_dependency class, which will be propagated by
 | 
						|
# iterating over _js_dist in __init__.py
 | 
						|
frame_open_template = """.{rpkgname}_js_metadata <- function() {{
 | 
						|
deps_metadata <- list("""
 | 
						|
 | 
						|
frame_element_template = """`{dep_name}` = structure(list(name = "{dep_name}",
 | 
						|
version = "{project_ver}", src = list(href = NULL,
 | 
						|
file = "deps"), meta = NULL,
 | 
						|
script = {script_name},
 | 
						|
stylesheet = {css_name}, head = NULL, attachment = NULL, package = "{rpkgname}",
 | 
						|
all_files = FALSE{async_or_dynamic}), class = "html_dependency")"""  # noqa:E501
 | 
						|
 | 
						|
frame_body_template = """`{project_shortname}` = structure(list(name = "{project_shortname}",
 | 
						|
version = "{project_ver}", src = list(href = NULL,
 | 
						|
file = "deps"), meta = NULL,
 | 
						|
script = {script_name},
 | 
						|
stylesheet = {css_name}, head = NULL, attachment = NULL, package = "{rpkgname}",
 | 
						|
all_files = FALSE{async_or_dynamic}), class = "html_dependency")"""  # noqa:E501
 | 
						|
 | 
						|
frame_close_template = """)
 | 
						|
return(deps_metadata)
 | 
						|
}
 | 
						|
"""
 | 
						|
 | 
						|
help_string = """% Auto-generated: do not edit by hand
 | 
						|
\\name{{{funcname}}}
 | 
						|
 | 
						|
\\alias{{{funcname}}}
 | 
						|
 | 
						|
\\title{{{name} component}}
 | 
						|
 | 
						|
\\description{{
 | 
						|
{description}
 | 
						|
}}
 | 
						|
 | 
						|
\\usage{{
 | 
						|
{funcname}({default_argtext})
 | 
						|
}}
 | 
						|
 | 
						|
\\arguments{{
 | 
						|
{item_text}
 | 
						|
}}
 | 
						|
 | 
						|
\\value{{{value_text}}}
 | 
						|
 | 
						|
"""
 | 
						|
 | 
						|
description_template = """Package: {package_name}
 | 
						|
Title: {package_title}
 | 
						|
Version: {package_version}
 | 
						|
Description: {package_description}
 | 
						|
Depends: R (>= 3.0.2){package_depends}
 | 
						|
Imports: {package_imports}
 | 
						|
Suggests: {package_suggests}{package_rauthors}
 | 
						|
License: {package_license}{package_copyright}
 | 
						|
URL: {package_url}
 | 
						|
BugReports: {package_issues}
 | 
						|
Encoding: UTF-8
 | 
						|
LazyData: true{vignette_builder}
 | 
						|
KeepSource: true
 | 
						|
"""
 | 
						|
 | 
						|
rbuild_ignore_string = r"""# ignore JS config files/folders
 | 
						|
node_modules/
 | 
						|
coverage/
 | 
						|
src/
 | 
						|
lib/
 | 
						|
.babelrc
 | 
						|
.builderrc
 | 
						|
.eslintrc
 | 
						|
.npmignore
 | 
						|
.editorconfig
 | 
						|
.eslintignore
 | 
						|
.prettierrc
 | 
						|
.circleci
 | 
						|
.github
 | 
						|
 | 
						|
# 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$
 | 
						|
"""
 | 
						|
 | 
						|
pkghelp_stub = """% Auto-generated: do not edit by hand
 | 
						|
\\docType{{package}}
 | 
						|
\\name{{{package_name}-package}}
 | 
						|
\\alias{{{package_name}}}
 | 
						|
\\title{{{pkg_help_title}}}
 | 
						|
\\description{{
 | 
						|
{pkg_help_description}
 | 
						|
}}
 | 
						|
\\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):
 | 
						|
    # Here we convert from snake case to camel case
 | 
						|
    package_name = snake_case_to_camel_case(project_shortname)
 | 
						|
 | 
						|
    # Ensure props are ordered with children first
 | 
						|
    props = reorder_props(props=props)
 | 
						|
 | 
						|
    prop_keys = list(props.keys())
 | 
						|
 | 
						|
    wildcards = ""
 | 
						|
    wildcard_declaration = ""
 | 
						|
    wildcard_names = ""
 | 
						|
    default_paramtext = ""
 | 
						|
    default_argtext = ""
 | 
						|
    accepted_wildcards = ""
 | 
						|
 | 
						|
    if any(key.endswith("-*") for key in prop_keys):
 | 
						|
        accepted_wildcards = get_wildcards_r(prop_keys)
 | 
						|
        wildcards = ", ..."
 | 
						|
        wildcard_declaration = wildcard_template.format(
 | 
						|
            accepted_wildcards.replace("-*", "")
 | 
						|
        )
 | 
						|
        wildcard_names = ", wildcard_names"
 | 
						|
 | 
						|
    # Produce a string with all property names other than WCs
 | 
						|
    prop_names = ", ".join(
 | 
						|
        "'{}'".format(p) for p in prop_keys if "*" not in p and p not in ["setProps"]
 | 
						|
    )
 | 
						|
 | 
						|
    # Filter props to remove those we don't want to expose
 | 
						|
    for item in prop_keys[:]:
 | 
						|
        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)
 | 
						|
 | 
						|
    # pylint: disable=C0301
 | 
						|
    default_paramtext += ", ".join(
 | 
						|
        "{0}={0}".format(p) if p != "children" else "{}=children".format(p)
 | 
						|
        for p in prop_keys
 | 
						|
    )
 | 
						|
 | 
						|
    return r_component_string.format(
 | 
						|
        funcname=format_fn_name(prefix, name),
 | 
						|
        name=name,
 | 
						|
        default_argtext=default_argtext,
 | 
						|
        wildcards=wildcards,
 | 
						|
        wildcard_declaration=wildcard_declaration,
 | 
						|
        default_paramtext=default_paramtext,
 | 
						|
        project_shortname=project_shortname,
 | 
						|
        prop_names=prop_names,
 | 
						|
        wildcard_names=wildcard_names,
 | 
						|
        package_name=package_name,
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
# pylint: disable=R0914
 | 
						|
def generate_js_metadata(pkg_data, project_shortname):
 | 
						|
    """Dynamically generate R function to supply JavaScript and CSS dependency
 | 
						|
    information required by the dash package for R.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    project_shortname = component library name, in snake case
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    function_string = complete R function code to provide component features
 | 
						|
    """
 | 
						|
    # make sure the module we're building is available to Python,
 | 
						|
    # even if it hasn't been installed yet
 | 
						|
    sys.path.insert(0, os.getcwd())
 | 
						|
    mod = importlib.import_module(project_shortname)
 | 
						|
 | 
						|
    alldist = getattr(mod, "_js_dist", []) + getattr(mod, "_css_dist", [])
 | 
						|
 | 
						|
    project_ver = pkg_data.get("version")
 | 
						|
 | 
						|
    rpkgname = snake_case_to_camel_case(project_shortname)
 | 
						|
 | 
						|
    # 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 concatenate
 | 
						|
    # them and write a list object in R with multiple elements
 | 
						|
    function_frame_open = frame_open_template.format(rpkgname=rpkgname)
 | 
						|
 | 
						|
    function_frame = []
 | 
						|
    function_frame_body = []
 | 
						|
 | 
						|
    # pylint: disable=consider-using-enumerate
 | 
						|
    if len(alldist) > 1:
 | 
						|
        for dep in range(len(alldist)):
 | 
						|
            curr_dep = alldist[dep]
 | 
						|
            rpp = curr_dep.get("relative_package_path", "")
 | 
						|
            if not rpp:
 | 
						|
                continue
 | 
						|
 | 
						|
            async_or_dynamic = get_async_type(curr_dep)
 | 
						|
 | 
						|
            if "dash_" in rpp:
 | 
						|
                dep_name = rpp.split(".")[0]
 | 
						|
            else:
 | 
						|
                dep_name = "{}".format(project_shortname)
 | 
						|
 | 
						|
            if "css" in rpp:
 | 
						|
                css_name = "'{}'".format(rpp)
 | 
						|
                script_name = "NULL"
 | 
						|
            else:
 | 
						|
                script_name = "'{}'".format(rpp)
 | 
						|
                css_name = "NULL"
 | 
						|
 | 
						|
            function_frame += [
 | 
						|
                frame_element_template.format(
 | 
						|
                    dep_name=dep_name,
 | 
						|
                    project_ver=project_ver,
 | 
						|
                    rpkgname=rpkgname,
 | 
						|
                    project_shortname=project_shortname,
 | 
						|
                    script_name=script_name,
 | 
						|
                    css_name=css_name,
 | 
						|
                    async_or_dynamic=async_or_dynamic,
 | 
						|
                )
 | 
						|
            ]
 | 
						|
            function_frame_body = ",\n".join(function_frame)
 | 
						|
    elif len(alldist) == 1:
 | 
						|
        dep = alldist[0]
 | 
						|
        rpp = dep["relative_package_path"]
 | 
						|
 | 
						|
        async_or_dynamic = get_async_type(dep)
 | 
						|
 | 
						|
        if "css" in rpp:
 | 
						|
            css_name = "'{}'".format(rpp)
 | 
						|
            script_name = "NULL"
 | 
						|
        else:
 | 
						|
            script_name = "'{}'".format(rpp)
 | 
						|
            css_name = "NULL"
 | 
						|
 | 
						|
        function_frame_body = frame_body_template.format(
 | 
						|
            project_shortname=project_shortname,
 | 
						|
            project_ver=project_ver,
 | 
						|
            rpkgname=rpkgname,
 | 
						|
            script_name=script_name,
 | 
						|
            css_name=css_name,
 | 
						|
            async_or_dynamic=async_or_dynamic,
 | 
						|
        )
 | 
						|
 | 
						|
    function_string = "".join(
 | 
						|
        [function_frame_open, function_frame_body, frame_close_template]
 | 
						|
    )
 | 
						|
 | 
						|
    return function_string
 | 
						|
 | 
						|
 | 
						|
# determine whether dependency uses async or dynamic flag
 | 
						|
# then return the properly formatted string if so, i.e.
 | 
						|
# " async = TRUE,". a dependency can have async or
 | 
						|
# dynamic elements, neither of these, but never both.
 | 
						|
def get_async_type(dep):
 | 
						|
    async_or_dynamic = ""
 | 
						|
    for key in dep.keys():
 | 
						|
        if key in ["async", "dynamic"]:
 | 
						|
            keyval = dep[key]
 | 
						|
            if not isinstance(keyval, bool):
 | 
						|
                keyval = "'{}'".format(keyval.lower())
 | 
						|
            else:
 | 
						|
                keyval = str(keyval).upper()
 | 
						|
            async_or_dynamic = ", {} = {}".format(key, keyval)
 | 
						|
    return async_or_dynamic
 | 
						|
 | 
						|
 | 
						|
# This method wraps code within arbitrary LaTeX-like tags, which are used
 | 
						|
# by R's internal help parser for constructing man pages
 | 
						|
def wrap(tag, code):
 | 
						|
    if tag == "":
 | 
						|
        return code
 | 
						|
    return "\\{}{{\n{}}}".format(tag, code)
 | 
						|
 | 
						|
 | 
						|
def write_help_file(name, props, description, prefix, rpkg_data):
 | 
						|
    """Write R documentation file (.Rd) given component name and properties.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    name = the name of the Dash component for which a help file is generated
 | 
						|
    props = the properties of the component
 | 
						|
    description = the component's description, inserted into help file header
 | 
						|
    prefix = the DashR library prefix (optional, can be a blank string)
 | 
						|
    rpkg_data = package metadata (optional)
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    writes an R help file to the man directory for the generated R package
 | 
						|
    """
 | 
						|
    funcname = format_fn_name(prefix, name)
 | 
						|
    file_name = funcname + ".Rd"
 | 
						|
 | 
						|
    wildcards = ""
 | 
						|
    default_argtext = ""
 | 
						|
    item_text = ""
 | 
						|
    accepted_wildcards = ""
 | 
						|
 | 
						|
    # the return value of all Dash components should be the same,
 | 
						|
    # in an abstract sense -- they produce a list
 | 
						|
    value_text = "named list of JSON elements corresponding to React.js properties and their values"  # noqa:E501
 | 
						|
 | 
						|
    prop_keys = list(props.keys())
 | 
						|
 | 
						|
    if any(key.endswith("-*") for key in prop_keys):
 | 
						|
        accepted_wildcards = get_wildcards_r(prop_keys)
 | 
						|
        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":
 | 
						|
            prop_keys.remove(item)
 | 
						|
 | 
						|
    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"]
 | 
						|
        )
 | 
						|
        for p in prop_keys
 | 
						|
    )
 | 
						|
 | 
						|
    # auto-replace any unescaped backslashes for compatibility with R docs
 | 
						|
    description = re.sub(r"(?<!\\)%", "\\%", description)
 | 
						|
    item_text = re.sub(r"(?<!\\)%", "\\%", item_text)
 | 
						|
 | 
						|
    # scrub examples which begin with **Example Usage**, as these should be
 | 
						|
    # provided as R code within dash-info.yaml
 | 
						|
    if "**Example Usage**" in description:
 | 
						|
        description = description.split("**Example Usage**")[0].rstrip()
 | 
						|
 | 
						|
    if wildcards == ", ...":
 | 
						|
        default_argtext += wildcards
 | 
						|
        item_text += wildcard_help_template.format(accepted_wildcards)
 | 
						|
 | 
						|
    # in R, the online help viewer does not properly wrap lines for
 | 
						|
    # the usage string -- we will hard wrap at 60 characters using
 | 
						|
    # textwrap.fill, starting from the beginning of the usage string
 | 
						|
 | 
						|
    file_path = os.path.join("man", file_name)
 | 
						|
    with open(file_path, "w", encoding="utf-8") as f:
 | 
						|
        f.write(
 | 
						|
            help_string.format(
 | 
						|
                funcname=funcname,
 | 
						|
                name=name,
 | 
						|
                default_argtext=textwrap.fill(
 | 
						|
                    default_argtext, width=60, break_long_words=False
 | 
						|
                ),
 | 
						|
                item_text=item_text,
 | 
						|
                value_text=value_text,
 | 
						|
                description=description.replace("\n", " "),
 | 
						|
            )
 | 
						|
        )
 | 
						|
    if rpkg_data is not None and "r_examples" in rpkg_data:
 | 
						|
        ex = rpkg_data.get("r_examples")
 | 
						|
        the_ex = ([e for e in ex if e.get("name") == funcname] or [None])[0]
 | 
						|
        result = ""
 | 
						|
        if the_ex and "code" in the_ex.keys():
 | 
						|
            result += wrap(
 | 
						|
                "examples",
 | 
						|
                wrap("dontrun" if the_ex.get("dontrun") else "", the_ex["code"]),
 | 
						|
            )
 | 
						|
            with open(file_path, "a+", encoding="utf-8") as fa:
 | 
						|
                fa.write(result + "\n")
 | 
						|
 | 
						|
 | 
						|
# pylint: disable=too-many-arguments
 | 
						|
def write_class_file(
 | 
						|
    name,
 | 
						|
    props,
 | 
						|
    description,
 | 
						|
    project_shortname,
 | 
						|
    prefix=None,
 | 
						|
    rpkg_data=None,
 | 
						|
):
 | 
						|
    props = reorder_props(props=props)
 | 
						|
 | 
						|
    # 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(name, props, description, prefix, rpkg_data)
 | 
						|
 | 
						|
    import_string = "# AUTO GENERATED FILE - DO NOT EDIT\n\n"
 | 
						|
    class_string = generate_class_string(name, props, project_shortname, prefix)
 | 
						|
 | 
						|
    file_name = format_fn_name(prefix, name) + ".R"
 | 
						|
 | 
						|
    file_path = os.path.join("R", file_name)
 | 
						|
    with open(file_path, "w", encoding="utf-8") as f:
 | 
						|
        f.write(import_string)
 | 
						|
        f.write(class_string)
 | 
						|
 | 
						|
    print("Generated {}".format(file_name))
 | 
						|
 | 
						|
 | 
						|
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.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    project_shortname = hyphenated string, e.g. dash-html-components
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    """
 | 
						|
    function_string = generate_js_metadata(
 | 
						|
        pkg_data=pkg_data, project_shortname=project_shortname
 | 
						|
    )
 | 
						|
    file_name = "internal.R"
 | 
						|
 | 
						|
    # the R source directory for the package won't exist on first call
 | 
						|
    # create the R directory if it is missing
 | 
						|
    if not os.path.exists("R"):
 | 
						|
        os.makedirs("R")
 | 
						|
 | 
						|
    file_path = os.path.join("R", file_name)
 | 
						|
    with open(file_path, "w", encoding="utf-8") 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
 | 
						|
    # create this directory if it is missing
 | 
						|
    if os.path.exists("inst/deps"):
 | 
						|
        shutil.rmtree("inst/deps")
 | 
						|
 | 
						|
    os.makedirs("inst/deps")
 | 
						|
 | 
						|
    for rel_dirname, _, filenames in os.walk(project_shortname):
 | 
						|
        for filename in filenames:
 | 
						|
            extension = os.path.splitext(filename)[1]
 | 
						|
 | 
						|
            if extension in [".py", ".pyc", ".json"]:
 | 
						|
                continue
 | 
						|
 | 
						|
            target_dirname = os.path.join(
 | 
						|
                os.path.join(
 | 
						|
                    "inst/deps/", os.path.relpath(rel_dirname, project_shortname)
 | 
						|
                )
 | 
						|
            )
 | 
						|
 | 
						|
            if not os.path.exists(target_dirname):
 | 
						|
                os.makedirs(target_dirname)
 | 
						|
 | 
						|
            shutil.copy(os.path.join(rel_dirname, filename), target_dirname)
 | 
						|
 | 
						|
 | 
						|
# pylint: disable=R0914, R0913, R0912, R0915
 | 
						|
def generate_rpkg(
 | 
						|
    pkg_data,
 | 
						|
    rpkg_data,
 | 
						|
    project_shortname,
 | 
						|
    export_string,
 | 
						|
    package_depends,
 | 
						|
    package_imports,
 | 
						|
    package_suggests,
 | 
						|
    has_wildcards,
 | 
						|
):
 | 
						|
    """Generate documents for R package creation.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    pkg_data
 | 
						|
    rpkg_data
 | 
						|
    project_shortname
 | 
						|
    export_string
 | 
						|
    package_depends
 | 
						|
    package_imports
 | 
						|
    package_suggests
 | 
						|
    has_wildcards
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    """
 | 
						|
    # Leverage package.json to import specifics which are also applicable
 | 
						|
    # to R package that we're generating here, use .get in case the key
 | 
						|
    # does not exist in package.json
 | 
						|
 | 
						|
    package_name = snake_case_to_camel_case(project_shortname)
 | 
						|
    package_copyright = ""
 | 
						|
    package_rauthors = ""
 | 
						|
    lib_name = pkg_data.get("name")
 | 
						|
 | 
						|
    if rpkg_data is not None:
 | 
						|
        if rpkg_data.get("pkg_help_title"):
 | 
						|
            package_title = rpkg_data.get(
 | 
						|
                "pkg_help_title", pkg_data.get("description", "")
 | 
						|
            )
 | 
						|
        if rpkg_data.get("pkg_help_description"):
 | 
						|
            package_description = rpkg_data.get(
 | 
						|
                "pkg_help_description", pkg_data.get("description", "")
 | 
						|
            )
 | 
						|
        if rpkg_data.get("pkg_copyright"):
 | 
						|
            package_copyright = "\nCopyright: {}".format(
 | 
						|
                rpkg_data.get("pkg_copyright", "")
 | 
						|
            )
 | 
						|
    else:
 | 
						|
        # fall back to using description in package.json, if present
 | 
						|
        package_title = pkg_data.get("description", "")
 | 
						|
        package_description = pkg_data.get("description", "")
 | 
						|
 | 
						|
    package_version = pkg_data.get("version", "0.0.1")
 | 
						|
 | 
						|
    # remove leading and trailing commas, add space after comma if missing
 | 
						|
    if package_depends:
 | 
						|
        package_depends = ", " + package_depends.strip(",").lstrip()
 | 
						|
        package_depends = re.sub(r"(,(?![ ]))", ", ", package_depends)
 | 
						|
 | 
						|
    if package_imports:
 | 
						|
        package_imports = package_imports.strip(",").lstrip()
 | 
						|
        package_imports = re.sub(r"(,(?![ ]))", ", ", package_imports)
 | 
						|
 | 
						|
    if package_suggests:
 | 
						|
        package_suggests = package_suggests.strip(",").lstrip()
 | 
						|
        package_suggests = re.sub(r"(,(?![ ]))", ", ", package_suggests)
 | 
						|
 | 
						|
    if "bugs" in pkg_data:
 | 
						|
        package_issues = pkg_data["bugs"].get("url", "")
 | 
						|
    else:
 | 
						|
        package_issues = ""
 | 
						|
        print(
 | 
						|
            "Warning: a URL for bug reports was "
 | 
						|
            "not provided. Empty string inserted.",
 | 
						|
            file=sys.stderr,
 | 
						|
        )
 | 
						|
 | 
						|
    if "homepage" in pkg_data:
 | 
						|
        package_url = pkg_data.get("homepage", "")
 | 
						|
    else:
 | 
						|
        package_url = ""
 | 
						|
        print(
 | 
						|
            "Warning: a homepage URL was not provided. Empty string inserted.",
 | 
						|
            file=sys.stderr,
 | 
						|
        )
 | 
						|
 | 
						|
    package_author = pkg_data.get("author")
 | 
						|
 | 
						|
    package_author_name = package_author.split(" <")[0]
 | 
						|
    package_author_email = package_author.split(" <")[1][:-1]
 | 
						|
 | 
						|
    package_author_fn = package_author_name.split(" ")[0]
 | 
						|
    package_author_ln = package_author_name.rsplit(" ", 2)[-1]
 | 
						|
 | 
						|
    maintainer = pkg_data.get("maintainer", pkg_data.get("author"))
 | 
						|
 | 
						|
    if "<" not in package_author:
 | 
						|
        print(
 | 
						|
            "Error, aborting R package generation: "
 | 
						|
            "R packages require a properly formatted author field "
 | 
						|
            "or installation will fail. Please include an email "
 | 
						|
            "address enclosed within < > brackets in package.json. ",
 | 
						|
            file=sys.stderr,
 | 
						|
        )
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
    if rpkg_data is not None:
 | 
						|
        if rpkg_data.get("pkg_authors"):
 | 
						|
            package_rauthors = "\nAuthors@R: {}".format(
 | 
						|
                rpkg_data.get("pkg_authors", "")
 | 
						|
            )
 | 
						|
        else:
 | 
						|
            package_rauthors = '\nAuthors@R: person("{}", "{}", role = c("aut", "cre"), email = "{}")'.format(
 | 
						|
                package_author_fn, package_author_ln, package_author_email
 | 
						|
            )
 | 
						|
 | 
						|
    if not (os.path.isfile("LICENSE") or os.path.isfile("LICENSE.txt")):
 | 
						|
        package_license = pkg_data.get("license", "")
 | 
						|
    else:
 | 
						|
        package_license = pkg_data.get("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"
 | 
						|
    packages_string = ""
 | 
						|
 | 
						|
    rpackage_list = package_depends.split(", ") + package_imports.split(", ")
 | 
						|
    rpackage_list = filter(bool, rpackage_list)
 | 
						|
 | 
						|
    if rpackage_list:
 | 
						|
        for rpackage in rpackage_list:
 | 
						|
            packages_string += "\nimport({})\n".format(rpackage)
 | 
						|
 | 
						|
    if os.path.exists("vignettes"):
 | 
						|
        vignette_builder = "\nVignetteBuilder: knitr"
 | 
						|
        if "knitr" not in package_suggests and "rmarkdown" not in package_suggests:
 | 
						|
            package_suggests += ", knitr, rmarkdown"
 | 
						|
            package_suggests = package_suggests.lstrip(", ")
 | 
						|
    else:
 | 
						|
        vignette_builder = ""
 | 
						|
 | 
						|
    pkghelp_stub_path = os.path.join("man", package_name + "-package.Rd")
 | 
						|
 | 
						|
    # 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, project_shortname, has_wildcards)
 | 
						|
 | 
						|
    with open("NAMESPACE", "w+", encoding="utf-8") as f:
 | 
						|
        f.write(import_string)
 | 
						|
        f.write(export_string)
 | 
						|
        f.write(packages_string)
 | 
						|
 | 
						|
    with open(".Rbuildignore", "w+", encoding="utf-8") as f2:
 | 
						|
        f2.write(rbuild_ignore_string)
 | 
						|
 | 
						|
    description_string = description_template.format(
 | 
						|
        package_name=package_name,
 | 
						|
        package_title=package_title,
 | 
						|
        package_description=package_description,
 | 
						|
        package_version=package_version,
 | 
						|
        package_rauthors=package_rauthors,
 | 
						|
        package_depends=package_depends,
 | 
						|
        package_imports=package_imports,
 | 
						|
        package_suggests=package_suggests,
 | 
						|
        package_license=package_license,
 | 
						|
        package_copyright=package_copyright,
 | 
						|
        package_url=package_url,
 | 
						|
        package_issues=package_issues,
 | 
						|
        vignette_builder=vignette_builder,
 | 
						|
    )
 | 
						|
 | 
						|
    with open("DESCRIPTION", "w+", encoding="utf-8") as f3:
 | 
						|
        f3.write(description_string)
 | 
						|
 | 
						|
    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", encoding="utf-8") as f4:
 | 
						|
                f4.write(pkghelp)
 | 
						|
 | 
						|
 | 
						|
# 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 snake_case_to_camel_case(namestring):
 | 
						|
    s = namestring.split("_")
 | 
						|
    return s[0] + "".join(w.capitalize() for w in s[1:])
 | 
						|
 | 
						|
 | 
						|
# this logic will permit passing blank R prefixes to
 | 
						|
# dash-generate-components, while also enforcing
 | 
						|
# camelCase for the resulting functions; if a prefix
 | 
						|
# is supplied, leave it as-is
 | 
						|
def format_fn_name(prefix, name):
 | 
						|
    if prefix:
 | 
						|
        return prefix + snake_case_to_camel_case(name)
 | 
						|
    return snake_case_to_camel_case(name[0].lower() + name[1:])
 | 
						|
 | 
						|
 | 
						|
# pylint: disable=unused-argument
 | 
						|
def generate_exports(
 | 
						|
    project_shortname,
 | 
						|
    components,
 | 
						|
    metadata,
 | 
						|
    pkg_data,
 | 
						|
    rpkg_data,
 | 
						|
    prefix,
 | 
						|
    package_depends,
 | 
						|
    package_imports,
 | 
						|
    package_suggests,
 | 
						|
    **kwargs
 | 
						|
):
 | 
						|
    export_string = make_namespace_exports(components, prefix)
 | 
						|
 | 
						|
    # 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,
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def make_namespace_exports(components, prefix):
 | 
						|
    export_string = ""
 | 
						|
    for component in components:
 | 
						|
        if (
 | 
						|
            not component.endswith("-*")
 | 
						|
            and str(component) not in r_keywords
 | 
						|
            and str(component) not in ["setProps", "children"]
 | 
						|
        ):
 | 
						|
            export_string += "export({}{})\n".format(prefix, component)
 | 
						|
 | 
						|
    # the following lines enable rudimentary support for bundling in
 | 
						|
    # R functions that are not automatically generated by the transpiler
 | 
						|
    # such that functions contained in the R subdirectory are exported,
 | 
						|
    # so long as they are not in utils.R.
 | 
						|
    rfilelist = []
 | 
						|
    omitlist = ["utils.R", "internal.R"] + [
 | 
						|
        "{}{}.R".format(prefix, component) for component in components
 | 
						|
    ]
 | 
						|
    fnlist = []
 | 
						|
 | 
						|
    for script in os.listdir("R"):
 | 
						|
        if script.endswith(".R") and script not in omitlist:
 | 
						|
            rfilelist += [os.path.join("R", script)]
 | 
						|
 | 
						|
    for rfile in rfilelist:
 | 
						|
        with open(rfile, "r", encoding="utf-8") as script:
 | 
						|
            s = script.read()
 | 
						|
 | 
						|
            # remove comments
 | 
						|
            s = re.sub("#.*$", "", s, flags=re.M)
 | 
						|
 | 
						|
            # put the whole file on one line
 | 
						|
            s = s.replace("\n", " ").replace("\r", " ")
 | 
						|
 | 
						|
            # empty out strings, in case of unmatched block terminators
 | 
						|
            s = re.sub(r"'([^'\\]|\\'|\\[^'])*'", "''", s)
 | 
						|
            s = re.sub(r'"([^"\\]|\\"|\\[^"])*"', '""', s)
 | 
						|
 | 
						|
            # empty out block terminators () and {}
 | 
						|
            # so we don't catch nested functions, or functions as arguments
 | 
						|
            # repeat until it stops changing, in case of multiply nested blocks
 | 
						|
            prev_len = len(s) + 1
 | 
						|
            while len(s) < prev_len:
 | 
						|
                prev_len = len(s)
 | 
						|
                s = re.sub(r"\(([^()]|\(\))*\)", "()", s)
 | 
						|
                s = re.sub(r"\{([^{}]|\{\})*\}", "{}", s)
 | 
						|
 | 
						|
            # now, in whatever is left, look for functions
 | 
						|
            matches = re.findall(
 | 
						|
                # in R, either = or <- may be used to create and assign objects
 | 
						|
                r"([^A-Za-z0-9._]|^)([A-Za-z0-9._]+)\s*(=|<-)\s*function",
 | 
						|
                s,
 | 
						|
            )
 | 
						|
            for match in matches:
 | 
						|
                fn = match[1]
 | 
						|
                # Allow users to mark functions as private by prefixing with .
 | 
						|
                if fn[0] != "." and fn not in fnlist:
 | 
						|
                    fnlist.append(fn)
 | 
						|
 | 
						|
    export_string += "\n".join("export({})".format(function) for function in fnlist)
 | 
						|
    return export_string
 | 
						|
 | 
						|
 | 
						|
def get_r_prop_types(type_object):
 | 
						|
    """Mapping from the PropTypes js type object to the R type."""
 | 
						|
 | 
						|
    def shape_or_exact():
 | 
						|
        return "lists containing elements {}.\n{}".format(
 | 
						|
            ", ".join("'{}'".format(t) for t in type_object["value"]),
 | 
						|
            "Those elements have the following types:\n{}".format(
 | 
						|
                "\n".join(
 | 
						|
                    create_prop_docstring_r(
 | 
						|
                        prop_name=prop_name,
 | 
						|
                        type_object=prop,
 | 
						|
                        required=prop["required"],
 | 
						|
                        description=prop.get("description", ""),
 | 
						|
                        indent_num=1,
 | 
						|
                    )
 | 
						|
                    for prop_name, prop in type_object["value"].items()
 | 
						|
                )
 | 
						|
            ),
 | 
						|
        )
 | 
						|
 | 
						|
    return dict(
 | 
						|
        array=lambda: "unnamed list",
 | 
						|
        bool=lambda: "logical",
 | 
						|
        number=lambda: "numeric",
 | 
						|
        string=lambda: "character",
 | 
						|
        object=lambda: "named list",
 | 
						|
        any=lambda: "logical | numeric | character | named list | unnamed list",
 | 
						|
        element=lambda: "dash component",
 | 
						|
        node=lambda: "a list of or a singular dash component, string or number",
 | 
						|
        # React's PropTypes.oneOf
 | 
						|
        enum=lambda: "a value equal to: {}".format(
 | 
						|
            ", ".join("{}".format(str(t["value"])) for t in type_object["value"])
 | 
						|
        ),
 | 
						|
        # React's PropTypes.oneOfType
 | 
						|
        union=lambda: "{}".format(
 | 
						|
            " | ".join(
 | 
						|
                "{}".format(get_r_type(subType))
 | 
						|
                for subType in type_object["value"]
 | 
						|
                if get_r_type(subType) != ""
 | 
						|
            )
 | 
						|
        ),
 | 
						|
        # React's PropTypes.arrayOf
 | 
						|
        arrayOf=lambda: (
 | 
						|
            "list"
 | 
						|
            + (
 | 
						|
                " of {}s".format(get_r_type(type_object["value"]))
 | 
						|
                if get_r_type(type_object["value"]) != ""
 | 
						|
                else ""
 | 
						|
            )
 | 
						|
        ),
 | 
						|
        # React's PropTypes.objectOf
 | 
						|
        objectOf=lambda: "list with named elements and values of type {}".format(
 | 
						|
            get_r_type(type_object["value"])
 | 
						|
        ),
 | 
						|
        # React's PropTypes.shape
 | 
						|
        shape=shape_or_exact,
 | 
						|
        # React's PropTypes.exact
 | 
						|
        exact=shape_or_exact,
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def get_r_type(type_object, is_flow_type=False, indent_num=0):
 | 
						|
    """
 | 
						|
    Convert JS types to R types for the component definition
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    type_object: dict
 | 
						|
        react-docgen-generated prop type dictionary
 | 
						|
    is_flow_type: bool
 | 
						|
    indent_num: int
 | 
						|
        Number of indents to use for the docstring for the prop
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    str
 | 
						|
        Python type string
 | 
						|
    """
 | 
						|
    js_type_name = type_object["name"]
 | 
						|
    js_to_r_types = get_r_prop_types(type_object=type_object)
 | 
						|
    if (
 | 
						|
        "computed" in type_object
 | 
						|
        and type_object["computed"]
 | 
						|
        or type_object.get("type", "") == "function"
 | 
						|
    ):
 | 
						|
        return ""
 | 
						|
    if js_type_name in js_to_r_types:
 | 
						|
        prop_type = js_to_r_types[js_type_name]()
 | 
						|
        return prop_type
 | 
						|
    return ""
 | 
						|
 | 
						|
 | 
						|
def print_r_type(typedata):
 | 
						|
    typestring = get_r_type(typedata).capitalize()
 | 
						|
    if typestring:
 | 
						|
        typestring += ". "
 | 
						|
    return typestring
 | 
						|
 | 
						|
 | 
						|
# pylint: disable=too-many-arguments
 | 
						|
def create_prop_docstring_r(
 | 
						|
    prop_name, type_object, required, description, indent_num, is_flow_type=False
 | 
						|
):
 | 
						|
    """
 | 
						|
    Create the Dash component prop docstring
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    prop_name: str
 | 
						|
        Name of the Dash component prop
 | 
						|
    type_object: dict
 | 
						|
        react-docgen-generated prop type dictionary
 | 
						|
    required: bool
 | 
						|
        Component is required?
 | 
						|
    description: str
 | 
						|
        Dash component description
 | 
						|
    indent_num: int
 | 
						|
        Number of indents to use for the context block
 | 
						|
        (creates 2 spaces for every indent)
 | 
						|
    is_flow_type: bool
 | 
						|
        Does the prop use Flow types? Otherwise, uses PropTypes
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    str
 | 
						|
        Dash component prop docstring
 | 
						|
    """
 | 
						|
    r_type_name = get_r_type(
 | 
						|
        type_object=type_object, is_flow_type=is_flow_type, indent_num=indent_num + 1
 | 
						|
    )
 | 
						|
 | 
						|
    indent_spacing = "  " * indent_num
 | 
						|
    if "\n" in r_type_name:
 | 
						|
        return (
 | 
						|
            "{indent_spacing}- {name} ({is_required}): {description}. "
 | 
						|
            "{name} has the following type: {type}".format(
 | 
						|
                indent_spacing=indent_spacing,
 | 
						|
                name=prop_name,
 | 
						|
                type=r_type_name,
 | 
						|
                description=description,
 | 
						|
                is_required="required" if required else "optional",
 | 
						|
            )
 | 
						|
        )
 | 
						|
    return "{indent_spacing}- {name} ({type}{is_required}){description}".format(
 | 
						|
        indent_spacing=indent_spacing,
 | 
						|
        name=prop_name,
 | 
						|
        type="{}; ".format(r_type_name) if r_type_name else "",
 | 
						|
        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
 |