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
|