550 lines
17 KiB
Python
550 lines
17 KiB
Python
|
# pylint: disable=consider-using-f-string
|
||
|
# type: ignore
|
||
|
import copy
|
||
|
import os
|
||
|
import shutil
|
||
|
import warnings
|
||
|
import sys
|
||
|
import importlib
|
||
|
import uuid
|
||
|
import hashlib
|
||
|
|
||
|
from ._all_keywords import julia_keywords
|
||
|
from ._py_components_generation import reorder_props
|
||
|
|
||
|
# uuid of DashBase Julia package.
|
||
|
jl_dash_base_uuid = "03207cf0-e2b3-4b91-9ca8-690cf0fb507e"
|
||
|
|
||
|
# uuid of Dash Julia package. Used as base for component package uuid
|
||
|
jl_dash_uuid = "1b08a953-4be3-4667-9a23-3db579824955"
|
||
|
|
||
|
# Declaring longer string templates as globals to improve
|
||
|
# readability, make method logic clearer to anyone inspecting
|
||
|
# code below
|
||
|
jl_component_string = '''
|
||
|
export {funcname}
|
||
|
|
||
|
"""
|
||
|
{funcname}(;kwargs...){children_signatures}
|
||
|
|
||
|
{docstring}
|
||
|
"""
|
||
|
function {funcname}(; kwargs...)
|
||
|
available_props = Symbol[{component_props}]
|
||
|
wild_props = Symbol[{wildcard_symbols}]
|
||
|
return Component("{funcname}", "{element_name}", "{module_name}", available_props, wild_props; kwargs...)
|
||
|
end
|
||
|
{children_definitions}
|
||
|
''' # noqa:E501
|
||
|
|
||
|
jl_children_signatures = """
|
||
|
{funcname}(children::Any;kwargs...)
|
||
|
{funcname}(children_maker::Function;kwargs...)
|
||
|
"""
|
||
|
|
||
|
jl_children_definitions = """
|
||
|
{funcname}(children::Any; kwargs...) = {funcname}(;kwargs..., children = children)
|
||
|
{funcname}(children_maker::Function; kwargs...) = {funcname}(children_maker(); kwargs...)
|
||
|
"""
|
||
|
|
||
|
jl_package_file_string = """
|
||
|
module {package_name}
|
||
|
using {base_package}
|
||
|
|
||
|
const resources_path = realpath(joinpath( @__DIR__, "..", "deps"))
|
||
|
const version = "{version}"
|
||
|
|
||
|
{component_includes}
|
||
|
|
||
|
function __init__()
|
||
|
DashBase.register_package(
|
||
|
DashBase.ResourcePkg(
|
||
|
"{project_shortname}",
|
||
|
resources_path,
|
||
|
version = version,
|
||
|
[
|
||
|
{resources_dist}
|
||
|
]
|
||
|
)
|
||
|
|
||
|
)
|
||
|
end
|
||
|
end
|
||
|
"""
|
||
|
|
||
|
jl_projecttoml_string = """
|
||
|
name = "{package_name}"
|
||
|
uuid = "{package_uuid}"
|
||
|
{authors}version = "{version}"
|
||
|
|
||
|
[deps]
|
||
|
{base_package} = "{dash_uuid}"
|
||
|
|
||
|
[compat]
|
||
|
julia = "1.2"
|
||
|
{base_package} = "{base_version}"
|
||
|
"""
|
||
|
|
||
|
jl_base_version = {
|
||
|
"Dash": "0.1.3, 1.0",
|
||
|
"DashBase": "0.1",
|
||
|
}
|
||
|
|
||
|
jl_component_include_string = 'include("jl/{name}.jl")'
|
||
|
|
||
|
jl_resource_tuple_string = """DashBase.Resource(
|
||
|
relative_package_path = {relative_package_path},
|
||
|
external_url = {external_url},
|
||
|
dynamic = {dynamic},
|
||
|
async = {async_string},
|
||
|
type = :{type}
|
||
|
)"""
|
||
|
|
||
|
core_packages = ["dash_html_components", "dash_core_components", "dash_table"]
|
||
|
|
||
|
|
||
|
def jl_package_name(namestring):
|
||
|
s = namestring.split("_")
|
||
|
return "".join(w.capitalize() for w in s)
|
||
|
|
||
|
|
||
|
def stringify_wildcards(wclist, no_symbol=False):
|
||
|
if no_symbol:
|
||
|
wcstring = "|".join("{}-".format(item) for item in wclist)
|
||
|
else:
|
||
|
wcstring = ", ".join('Symbol("{}-")'.format(item) for item in wclist)
|
||
|
return wcstring
|
||
|
|
||
|
|
||
|
def get_wildcards_jl(props):
|
||
|
return [key.replace("-*", "") for key in props if key.endswith("-*")]
|
||
|
|
||
|
|
||
|
def get_jl_prop_types(type_object):
|
||
|
"""Mapping from the PropTypes js type object to the Julia 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_jl(
|
||
|
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: "Array",
|
||
|
bool=lambda: "Bool",
|
||
|
number=lambda: "Real",
|
||
|
string=lambda: "String",
|
||
|
object=lambda: "Dict",
|
||
|
any=lambda: "Bool | Real | String | Dict | Array",
|
||
|
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_jl_type(subType))
|
||
|
for subType in type_object["value"]
|
||
|
if get_jl_type(subType) != ""
|
||
|
)
|
||
|
),
|
||
|
# React's PropTypes.arrayOf
|
||
|
arrayOf=lambda: (
|
||
|
"Array"
|
||
|
+ (
|
||
|
" of {}s".format(get_jl_type(type_object["value"]))
|
||
|
if get_jl_type(type_object["value"]) != ""
|
||
|
else ""
|
||
|
)
|
||
|
),
|
||
|
# React's PropTypes.objectOf
|
||
|
objectOf=lambda: "Dict with Strings as keys and values of type {}".format(
|
||
|
get_jl_type(type_object["value"])
|
||
|
),
|
||
|
# React's PropTypes.shape
|
||
|
shape=shape_or_exact,
|
||
|
# React's PropTypes.exact
|
||
|
exact=shape_or_exact,
|
||
|
)
|
||
|
|
||
|
|
||
|
def filter_props(props):
|
||
|
"""Filter props from the Component arguments to exclude:
|
||
|
- Those without a "type" or a "flowType" field
|
||
|
- Those with arg.type.name in {'func', 'symbol', 'instanceOf'}
|
||
|
Parameters
|
||
|
----------
|
||
|
props: dict
|
||
|
Dictionary with {propName: propMetadata} structure
|
||
|
Returns
|
||
|
-------
|
||
|
dict
|
||
|
Filtered dictionary with {propName: propMetadata} structure
|
||
|
"""
|
||
|
filtered_props = copy.deepcopy(props)
|
||
|
|
||
|
for arg_name, arg in list(filtered_props.items()):
|
||
|
if "type" not in arg and "flowType" not in arg:
|
||
|
filtered_props.pop(arg_name)
|
||
|
continue
|
||
|
|
||
|
# Filter out functions and instances --
|
||
|
if "type" in arg: # These come from PropTypes
|
||
|
arg_type = arg["type"]["name"]
|
||
|
if arg_type in {"func", "symbol", "instanceOf"}:
|
||
|
filtered_props.pop(arg_name)
|
||
|
elif "flowType" in arg: # These come from Flow & handled differently
|
||
|
arg_type_name = arg["flowType"]["name"]
|
||
|
if arg_type_name == "signature":
|
||
|
# This does the same as the PropTypes filter above, but "func"
|
||
|
# is under "type" if "name" is "signature" vs just in "name"
|
||
|
if "type" not in arg["flowType"] or arg["flowType"]["type"] != "object":
|
||
|
filtered_props.pop(arg_name)
|
||
|
else:
|
||
|
raise ValueError
|
||
|
|
||
|
return filtered_props
|
||
|
|
||
|
|
||
|
def get_jl_type(type_object):
|
||
|
"""
|
||
|
Convert JS types to Julia types for the component definition
|
||
|
Parameters
|
||
|
----------
|
||
|
type_object: dict
|
||
|
react-docgen-generated prop type dictionary
|
||
|
Returns
|
||
|
-------
|
||
|
str
|
||
|
Julia type string
|
||
|
"""
|
||
|
js_type_name = type_object["name"]
|
||
|
js_to_jl_types = get_jl_prop_types(type_object=type_object)
|
||
|
if js_type_name in js_to_jl_types:
|
||
|
prop_type = js_to_jl_types[js_type_name]()
|
||
|
return prop_type
|
||
|
return ""
|
||
|
|
||
|
|
||
|
def print_jl_type(typedata):
|
||
|
typestring = get_jl_type(typedata).capitalize()
|
||
|
if typestring:
|
||
|
typestring += ". "
|
||
|
return typestring
|
||
|
|
||
|
|
||
|
def create_docstring_jl(component_name, props, description):
|
||
|
"""Create the Dash component docstring.
|
||
|
Parameters
|
||
|
----------
|
||
|
component_name: str
|
||
|
Component name
|
||
|
props: dict
|
||
|
Dictionary with {propName: propMetadata} structure
|
||
|
description: str
|
||
|
Component description
|
||
|
Returns
|
||
|
-------
|
||
|
str
|
||
|
Dash component docstring
|
||
|
"""
|
||
|
# Ensure props are ordered with children first
|
||
|
props = reorder_props(props=props)
|
||
|
|
||
|
return "A{n} {name} component.\n{description}\nKeyword arguments:\n{args}".format(
|
||
|
n="n" if component_name[0].lower() in "aeiou" else "",
|
||
|
name=component_name,
|
||
|
description=description,
|
||
|
args="\n".join(
|
||
|
create_prop_docstring_jl(
|
||
|
prop_name=p,
|
||
|
type_object=prop["type"] if "type" in prop else prop["flowType"],
|
||
|
required=prop["required"],
|
||
|
description=prop["description"],
|
||
|
indent_num=0,
|
||
|
)
|
||
|
for p, prop in filter_props(props).items()
|
||
|
),
|
||
|
)
|
||
|
|
||
|
|
||
|
def create_prop_docstring_jl(
|
||
|
prop_name,
|
||
|
type_object,
|
||
|
required,
|
||
|
description,
|
||
|
indent_num,
|
||
|
):
|
||
|
"""
|
||
|
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
|
||
|
"""
|
||
|
jl_type_name = get_jl_type(type_object=type_object)
|
||
|
|
||
|
indent_spacing = " " * indent_num
|
||
|
if "\n" in jl_type_name:
|
||
|
return (
|
||
|
"{indent_spacing}- `{name}` ({is_required}): {description}. "
|
||
|
"{name} has the following type: {type}".format(
|
||
|
indent_spacing=indent_spacing,
|
||
|
name=prop_name,
|
||
|
type=jl_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(jl_type_name) if jl_type_name else "",
|
||
|
description=(": {}".format(description) if description != "" else ""),
|
||
|
is_required="required" if required else "optional",
|
||
|
)
|
||
|
|
||
|
|
||
|
# this logic will permit passing blank Julia prefixes to
|
||
|
# dash-generate-components, while also enforcing
|
||
|
# lower case names for the resulting functions; if a prefix
|
||
|
# is supplied, leave it as-is
|
||
|
def format_fn_name(prefix, name):
|
||
|
if prefix:
|
||
|
return "{}_{}".format(prefix, name.lower())
|
||
|
return name.lower()
|
||
|
|
||
|
|
||
|
def generate_metadata_strings(resources, metatype):
|
||
|
def nothing_or_string(v):
|
||
|
return '"{}"'.format(v) if v else "nothing"
|
||
|
|
||
|
return [
|
||
|
jl_resource_tuple_string.format(
|
||
|
relative_package_path=nothing_or_string(
|
||
|
resource.get("relative_package_path", "")
|
||
|
),
|
||
|
external_url=nothing_or_string(resource.get("external_url", "")),
|
||
|
dynamic=str(resource.get("dynamic", "nothing")).lower(),
|
||
|
type=metatype,
|
||
|
async_string=":{}".format(str(resource.get("async")).lower())
|
||
|
if "async" in resource.keys()
|
||
|
else "nothing",
|
||
|
)
|
||
|
for resource in resources
|
||
|
]
|
||
|
|
||
|
|
||
|
def is_core_package(project_shortname):
|
||
|
return project_shortname in core_packages
|
||
|
|
||
|
|
||
|
def base_package_name(project_shortname):
|
||
|
return "DashBase" if is_core_package(project_shortname) else "Dash"
|
||
|
|
||
|
|
||
|
def base_package_uid(project_shortname):
|
||
|
return jl_dash_base_uuid if is_core_package(project_shortname) else jl_dash_uuid
|
||
|
|
||
|
|
||
|
def generate_package_file(project_shortname, components, pkg_data, prefix):
|
||
|
package_name = jl_package_name(project_shortname)
|
||
|
|
||
|
sys.path.insert(0, os.getcwd())
|
||
|
mod = importlib.import_module(project_shortname)
|
||
|
js_dist = getattr(mod, "_js_dist", [])
|
||
|
css_dist = getattr(mod, "_css_dist", [])
|
||
|
project_ver = pkg_data.get("version")
|
||
|
|
||
|
resources_dist = ",\n".join(
|
||
|
generate_metadata_strings(js_dist, "js")
|
||
|
+ generate_metadata_strings(css_dist, "css")
|
||
|
)
|
||
|
|
||
|
package_string = jl_package_file_string.format(
|
||
|
package_name=package_name,
|
||
|
component_includes="\n".join(
|
||
|
[
|
||
|
jl_component_include_string.format(
|
||
|
name=format_fn_name(prefix, comp_name)
|
||
|
)
|
||
|
for comp_name in components
|
||
|
]
|
||
|
),
|
||
|
resources_dist=resources_dist,
|
||
|
version=project_ver,
|
||
|
project_shortname=project_shortname,
|
||
|
base_package=base_package_name(project_shortname),
|
||
|
)
|
||
|
file_path = os.path.join("src", package_name + ".jl")
|
||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||
|
f.write(package_string)
|
||
|
print("Generated {}".format(file_path))
|
||
|
|
||
|
|
||
|
def generate_toml_file(project_shortname, pkg_data):
|
||
|
package_author = pkg_data.get("author", "")
|
||
|
project_ver = pkg_data.get("version")
|
||
|
package_name = jl_package_name(project_shortname)
|
||
|
u = uuid.UUID(jl_dash_uuid)
|
||
|
|
||
|
package_uuid = uuid.UUID(
|
||
|
hex=u.hex[:-12] + hashlib.sha256(package_name.encode("utf-8")).hexdigest()[-12:]
|
||
|
)
|
||
|
|
||
|
authors_string = (
|
||
|
'authors = ["{}"]\n'.format(package_author) if package_author else ""
|
||
|
)
|
||
|
|
||
|
base_package = base_package_name(project_shortname)
|
||
|
|
||
|
toml_string = jl_projecttoml_string.format(
|
||
|
package_name=package_name,
|
||
|
package_uuid=package_uuid,
|
||
|
version=project_ver,
|
||
|
authors=authors_string,
|
||
|
base_package=base_package,
|
||
|
base_version=jl_base_version[base_package],
|
||
|
dash_uuid=base_package_uid(project_shortname),
|
||
|
)
|
||
|
file_path = "Project.toml"
|
||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||
|
f.write(toml_string)
|
||
|
print("Generated {}".format(file_path))
|
||
|
|
||
|
|
||
|
def generate_class_string(name, props, description, project_shortname, prefix):
|
||
|
# Ensure props are ordered with children first
|
||
|
filtered_props = reorder_props(filter_props(props))
|
||
|
|
||
|
prop_keys = list(filtered_props.keys())
|
||
|
|
||
|
docstring = (
|
||
|
create_docstring_jl(
|
||
|
component_name=name, props=filtered_props, description=description
|
||
|
)
|
||
|
.replace("\r\n", "\n")
|
||
|
.replace("$", "\\$")
|
||
|
)
|
||
|
|
||
|
wclist = get_wildcards_jl(props)
|
||
|
default_paramtext = ""
|
||
|
|
||
|
# 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 julia_keywords:
|
||
|
prop_keys.remove(item)
|
||
|
warnings.warn(
|
||
|
(
|
||
|
'WARNING: prop "{}" in component "{}" is a Julia keyword'
|
||
|
" - REMOVED FROM THE JULIA COMPONENT"
|
||
|
).format(item, name)
|
||
|
)
|
||
|
|
||
|
default_paramtext += ", ".join(":{}".format(p) for p in prop_keys)
|
||
|
|
||
|
has_children = "children" in prop_keys
|
||
|
funcname = format_fn_name(prefix, name)
|
||
|
children_signatures = (
|
||
|
jl_children_signatures.format(funcname=funcname) if has_children else ""
|
||
|
)
|
||
|
children_definitions = (
|
||
|
jl_children_definitions.format(funcname=funcname) if has_children else ""
|
||
|
)
|
||
|
return jl_component_string.format(
|
||
|
funcname=format_fn_name(prefix, name),
|
||
|
docstring=docstring,
|
||
|
component_props=default_paramtext,
|
||
|
wildcard_symbols=stringify_wildcards(wclist, no_symbol=False),
|
||
|
wildcard_names=stringify_wildcards(wclist, no_symbol=True),
|
||
|
element_name=name,
|
||
|
module_name=project_shortname,
|
||
|
children_signatures=children_signatures,
|
||
|
children_definitions=children_definitions,
|
||
|
)
|
||
|
|
||
|
|
||
|
def generate_struct_file(name, props, description, project_shortname, prefix):
|
||
|
props = reorder_props(props=props)
|
||
|
import_string = "# AUTO GENERATED FILE - DO NOT EDIT\n"
|
||
|
class_string = generate_class_string(
|
||
|
name, props, description, project_shortname, prefix
|
||
|
)
|
||
|
|
||
|
file_name = format_fn_name(prefix, name) + ".jl"
|
||
|
|
||
|
# put component files in src/jl subdir,
|
||
|
# this also creates the Julia source directory for the package
|
||
|
# if it is missing
|
||
|
if not os.path.exists("src/jl"):
|
||
|
os.makedirs("src/jl")
|
||
|
|
||
|
file_path = os.path.join("src", "jl", 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))
|
||
|
|
||
|
|
||
|
# pylint: disable=unused-argument
|
||
|
def generate_module(
|
||
|
project_shortname, components, metadata, pkg_data, prefix, **kwargs
|
||
|
):
|
||
|
# 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("deps"):
|
||
|
shutil.rmtree("deps")
|
||
|
|
||
|
os.makedirs("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(
|
||
|
"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)
|
||
|
|
||
|
generate_package_file(project_shortname, components, pkg_data, prefix)
|
||
|
generate_toml_file(project_shortname, pkg_data)
|