Repository: plotly/dash

Issue:
Modify 'transpiler' behaviour when empty string passed as R component prefix
Currently, R components may be generated for Dash using the syntax

`dash-generate-components ./src/components dash_core_components --r-prefix="dcc"`

When `--r-prefix` is blank, the 'transpiler' assumes `dash` is the prefix. This is undesirable for third party components, or libraries such as `dash-canvas`, which already include "dash" in their names.

Alternatively, a better strategy for the 'transpiler' behaviour might be to accept a blank string, e.g.

`dash-generate-components ./src/lib/components dash_canvas --r-prefix=""`

Then, then the 'transpiler' could generate a properly camelCase function name, i.e. `dashCanvas` instead of `DashCanvas`. 

This would maintain parity with existing Plotly component libraries, and further standardize function naming. These details seem minor, but as R is case-sensitive, they remain important.

@alexcjohnson 


Discussion:
Yes, `--r-prefix=""` converting to camelCase sounds perfect. Good catch!

Resolving patch:
diff --git a/dash/development/_r_components_generation.py b/dash/development/_r_components_generation.py
--- a/dash/development/_r_components_generation.py
+++ b/dash/development/_r_components_generation.py
@@ -16,7 +16,7 @@
 # Declaring longer string templates as globals to improve
 # readability, make method logic clearer to anyone inspecting
 # code below
-r_component_string = """{prefix}{name} <- function({default_argtext}{wildcards}) {{
+r_component_string = """{funcname} <- function({default_argtext}{wildcards}) {{
     {wildcard_declaration}
     component <- list(
         props = list({default_paramtext}{wildcards}),
@@ -60,9 +60,9 @@
 """
 
 help_string = """% Auto-generated: do not edit by hand
-\\name{{{prefix}{name}}}
+\\name{{{funcname}}}
 
-\\alias{{{prefix}{name}}}
+\\alias{{{funcname}}}
 
 \\title{{{name} component}}
 
@@ -71,7 +71,7 @@
 }}
 
 \\usage{{
-{argtext}
+{funcname}({default_argtext})
 }}
 
 \\arguments{{
@@ -205,18 +205,16 @@ def generate_class_string(name, props, project_shortname, prefix):
         for p in prop_keys
     )
 
-    return r_component_string.format(
-        prefix=prefix,
-        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,
-    )
+    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
@@ -319,7 +317,7 @@ def write_help_file(name, props, description, prefix):
     writes an R help file to the man directory for the generated R package
 
     """
-    file_name = "{}{}.Rd".format(prefix, name)
+    file_name = format_fn_name(prefix, name) + ".Rd"
 
     default_argtext = ""
     item_text = ""
@@ -356,32 +354,23 @@ def write_help_file(name, props, description, prefix):
     file_path = os.path.join('man', file_name)
     with open(file_path, 'w') as f:
         f.write(help_string.format(
-            prefix=prefix,
+            funcname=format_fn_name(prefix, name),
             name=name,
-            argtext=textwrap.fill(argtext,
-                                  width=80,
-                                  break_long_words=False),
+            default_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):
+def write_class_file(name,
+                     props,
+                     description,
+                     project_shortname,
+                     prefix=None):
     props = reorder_props(props=props)
 
-    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
@@ -394,6 +383,22 @@ def write_class_file(name, props, description, project_shortname, prefix=None):
         prefix
     )
 
+    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") as f:
+        f.write(import_string)
+        f.write(class_string)
+
     print("Generated {}".format(file_name))
 
 
@@ -596,6 +601,16 @@ def snake_case_to_camel_case(namestring):
     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,
diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py
--- a/dash/development/component_generator.py
+++ b/dash/development/component_generator.py
@@ -50,7 +50,7 @@ def generate_components(
 
     project_shortname = project_shortname.replace("-", "_").rstrip("/\\")
 
-    if rprefix:
+    if rprefix is not None:
         prefix = rprefix
 
     is_windows = sys.platform == "win32"
@@ -98,25 +98,27 @@ def generate_components(
 
     generator_methods = [generate_class_file]
 
-    if rprefix:
-        if not os.path.exists("man"):
-            os.makedirs("man")
-        if not os.path.exists("R"):
-            os.makedirs("R")
-        generator_methods.append(functools.partial(write_class_file,
-                                                   prefix=prefix))
+    if rprefix is not None:
+        if not os.path.exists('man'):
+            os.makedirs('man')
+        if not os.path.exists('R'):
+            os.makedirs('R')
+        generator_methods.append(
+            functools.partial(write_class_file, prefix=prefix))
 
-    components = generate_classes_files(project_shortname,
-                                        metadata,
-                                        *generator_methods)
+    components = generate_classes_files(
+        project_shortname,
+        metadata,
+        *generator_methods
+    )
 
-    with open(os.path.join(project_shortname, "metadata.json"), "w") as f:
+    with open(os.path.join(project_shortname, 'metadata.json'), 'w') as f:
         json.dump(metadata, f, indent=2)
 
     generate_imports(project_shortname, components)
 
-    if rprefix:
-        with open("package.json", "r") as f:
+    if rprefix is not None:
+        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


Test patch: