Skip to content

Commit

Permalink
Provide details on component type in R online help for transpiled lib…
Browse files Browse the repository at this point in the history
…raries (#726)

* 🎉 initial support for argument typing in DashR

* 🔨 modify types list/dict to unnamed/named list in R

* 🔨 union types now properly formatted in R

* 🔨 collect CSS dependencies within R 📦
  • Loading branch information
rpkyle committed Jun 9, 2019
1 parent c2659ae commit c0c581b
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 52 deletions.
11 changes: 6 additions & 5 deletions dash/development/_py_components_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,11 +488,12 @@ def shape_or_exact():
if js_to_py_type(subType) != '')),

# React's PropTypes.arrayOf
arrayOf=lambda: 'list'.format( # pylint: disable=too-many-format-args
' of {}s'.format(
js_to_py_type(type_object['value']))
if js_to_py_type(type_object['value']) != ''
else ''),
arrayOf=lambda: (
"list" + ((" of {}s").format(
js_to_py_type(type_object["value"]))
if js_to_py_type(type_object["value"]) != ""
else "")
),

# React's PropTypes.objectOf
objectOf=lambda: (
Expand Down
256 changes: 220 additions & 36 deletions dash/development/_r_components_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import shutil
import glob
import importlib
import textwrap
import re

from ._all_keywords import r_keywords
Expand Down Expand Up @@ -41,15 +42,15 @@
frame_element_template = """`{dep_name}` = structure(list(name = "{dep_name}",
version = "{project_ver}", src = list(href = NULL,
file = "deps"), meta = NULL,
script = "{dep_rpp}",
stylesheet = NULL, head = NULL, attachment = NULL, package = "{rpkgname}",
all_files = FALSE), class = "html_dependency")"""
script = {script_name},
stylesheet = {css_name}, head = NULL, attachment = NULL, package = "{rpkgname}",
all_files = FALSE), 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 = "{dep_rpp}",
stylesheet = NULL, head = NULL, attachment = NULL, package = "{rpkgname}",
script = {script_name},
stylesheet = {css_name}, head = NULL, attachment = NULL, package = "{rpkgname}",
all_files = FALSE), class = "html_dependency")""" # noqa:E501

frame_close_template = """)
Expand All @@ -70,7 +71,7 @@
}}
\\usage{{
{prefix}{name}({default_argtext})
{argtext}
}}
\\arguments{{
Expand Down Expand Up @@ -237,7 +238,8 @@ def generate_js_metadata(pkg_data, project_shortname):
# import component library module into sys
mod = sys.modules[project_shortname]

jsdist = getattr(mod, "_js_dist", [])
alldist = getattr(mod, "_js_dist", []) + getattr(mod, "_css_dist", [])

project_ver = pkg_data.get("version")

rpkgname = snake_case_to_camel_case(project_shortname)
Expand All @@ -253,29 +255,45 @@ def generate_js_metadata(pkg_data, project_shortname):
function_frame_body = []

# pylint: disable=consider-using-enumerate
if len(jsdist) > 1:
for dep in range(len(jsdist)):
if "dash_" in jsdist[dep]["relative_package_path"]:
dep_name = jsdist[dep]["relative_package_path"].split(".")[0]
if len(alldist) > 1:
for dep in range(len(alldist)):
rpp = alldist[dep]["relative_package_path"]
if "dash_" in rpp:
dep_name = rpp.split(".")[0]
else:
dep_name = "{}_{}".format(project_shortname, str(dep))
project_ver = str(dep)
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,
dep_rpp=jsdist[dep]["relative_package_path"],
script_name=script_name,
css_name=css_name,
)
]
function_frame_body = ",\n".join(function_frame)
elif len(jsdist) == 1:
elif len(alldist) == 1:
rpp = alldist[0]["relative_package_path"]
if "css" in rpp:
css_name = rpp
script_name = "NULL"
else:
script_name = rpp
css_name = "NULL"
function_frame_body = frame_body_template.format(
project_shortname=project_shortname,
project_ver=project_ver,
rpkgname=rpkgname,
dep_rpp=jsdist[0]["relative_package_path"],
script_name=script_name,
css_name=css_name,
)

function_string = "".join(
Expand Down Expand Up @@ -318,49 +336,64 @@ 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, 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-*`}"
default_argtext += ", ..."

file_path = os.path.join("man", file_name)
with open(file_path, "w") as f:
f.write(
help_string.format(
prefix=prefix,
name=name,
default_argtext=default_argtext,
item_text=item_text,
description=description.replace("\n", " "),
)
)
item_text += '\n\n\\item{...}{wildcards: `data-*` or `aria-*`}'
default_argtext += ', ...'

# 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(
prefix=prefix,
name=name,
argtext=textwrap.fill(argtext,
width=80,
break_long_words=False),
item_text=item_text,
description=description.replace('\n', ' ')
))


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
# 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)

import_string = "# AUTO GENERATED FILE - DO NOT EDIT\n\n"
class_string = generate_class_string(name,
props,
project_shortname,
prefix)

file_name = "{}{}.R".format(prefix, name)

file_path = os.path.join("R", file_name)
with open(file_path, "w") as f:
f.write(import_string)
f.write(class_string)

# 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
)

print("Generated {}".format(file_name))


Expand Down Expand Up @@ -625,3 +658,154 @@ def generate_exports(
package_imports,
package_suggests,
)


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 list(type_object['value'].keys())
),
'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
list(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
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 ""
elif 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')
6 changes: 3 additions & 3 deletions tests/unit/dash/development/metadata_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@ class Table(Component):
- optionalElement (dash component; optional)
- optionalEnum (a value equal to: 'News', 'Photos'; optional)
- optionalUnion (string | number; optional)
- optionalArrayOf (list; optional)
- optionalArrayOf (list of numbers; optional)
- optionalObjectOf (dict with strings as keys and values of type number; optional)
- optionalObjectWithExactAndNestedDescription (optional): . optionalObjectWithExactAndNestedDescription has the following type: dict containing keys 'color', 'fontSize', 'figure'.
Those keys have the following types:
- color (string; optional)
- fontSize (number; optional)
- figure (optional): Figure is a plotly graph object. figure has the following type: dict containing keys 'data', 'layout'.
Those keys have the following types:
- data (list; optional): data is a collection of traces
- data (list of dicts; optional): data is a collection of traces
- layout (dict; optional): layout describes the rest of the figure
- optionalObjectWithShapeAndNestedDescription (optional): . optionalObjectWithShapeAndNestedDescription has the following type: dict containing keys 'color', 'fontSize', 'figure'.
Those keys have the following types:
- color (string; optional)
- fontSize (number; optional)
- figure (optional): Figure is a plotly graph object. figure has the following type: dict containing keys 'data', 'layout'.
Those keys have the following types:
- data (list; optional): data is a collection of traces
- data (list of dicts; optional): data is a collection of traces
- layout (dict; optional): layout describes the rest of the figure
- optionalAny (boolean | number | string | dict | list; optional)
- customProp (optional)
Expand Down
Loading

0 comments on commit c0c581b

Please sign in to comment.