297 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			297 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from collections import OrderedDict
 | 
						|
 | 
						|
import json
 | 
						|
import sys
 | 
						|
import subprocess
 | 
						|
import shlex
 | 
						|
import os
 | 
						|
import argparse
 | 
						|
import shutil
 | 
						|
import functools
 | 
						|
import pkg_resources
 | 
						|
import yaml
 | 
						|
 | 
						|
from ._r_components_generation import write_class_file
 | 
						|
from ._r_components_generation import generate_exports
 | 
						|
from ._py_components_generation import generate_class_file
 | 
						|
from ._py_components_generation import generate_imports
 | 
						|
from ._py_components_generation import generate_classes_files
 | 
						|
from ._jl_components_generation import generate_struct_file
 | 
						|
from ._jl_components_generation import generate_module
 | 
						|
from ._generate_prop_types import generate_prop_types
 | 
						|
 | 
						|
reserved_words = [
 | 
						|
    "UNDEFINED",
 | 
						|
    "REQUIRED",
 | 
						|
    "to_plotly_json",
 | 
						|
    "available_properties",
 | 
						|
    "available_wildcard_properties",
 | 
						|
    "_.*",
 | 
						|
]
 | 
						|
 | 
						|
 | 
						|
class _CombinedFormatter(
 | 
						|
    argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter
 | 
						|
):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
# pylint: disable=too-many-locals, too-many-arguments, too-many-branches, too-many-statements
 | 
						|
def generate_components(
 | 
						|
    components_source,
 | 
						|
    project_shortname,
 | 
						|
    package_info_filename="package.json",
 | 
						|
    ignore="^_",
 | 
						|
    rprefix=None,
 | 
						|
    rdepends="",
 | 
						|
    rimports="",
 | 
						|
    rsuggests="",
 | 
						|
    jlprefix=None,
 | 
						|
    metadata=None,
 | 
						|
    keep_prop_order=None,
 | 
						|
    max_props=None,
 | 
						|
    custom_typing_module=None,
 | 
						|
):
 | 
						|
 | 
						|
    project_shortname = project_shortname.replace("-", "_").rstrip("/\\")
 | 
						|
 | 
						|
    is_windows = sys.platform == "win32"
 | 
						|
 | 
						|
    extract_path = pkg_resources.resource_filename("dash", "extract-meta.js")
 | 
						|
 | 
						|
    reserved_patterns = "|".join(f"^{p}$" for p in reserved_words)
 | 
						|
 | 
						|
    os.environ["NODE_PATH"] = "node_modules"
 | 
						|
 | 
						|
    shutil.copyfile(
 | 
						|
        "package.json", os.path.join(project_shortname, package_info_filename)
 | 
						|
    )
 | 
						|
 | 
						|
    if not metadata:
 | 
						|
        env = os.environ.copy()
 | 
						|
 | 
						|
        # Ensure local node modules is used when the script is packaged.
 | 
						|
        env["MODULES_PATH"] = os.path.abspath("./node_modules")
 | 
						|
 | 
						|
        cmd = shlex.split(
 | 
						|
            f'node {extract_path} "{ignore}" "{reserved_patterns}" {components_source}',
 | 
						|
            posix=not is_windows,
 | 
						|
        )
 | 
						|
 | 
						|
        proc = subprocess.Popen(  # pylint: disable=consider-using-with
 | 
						|
            cmd,
 | 
						|
            stdout=subprocess.PIPE,
 | 
						|
            stderr=subprocess.PIPE,
 | 
						|
            shell=is_windows,
 | 
						|
            env=env,
 | 
						|
        )
 | 
						|
        out, err = proc.communicate()
 | 
						|
        status = proc.poll()
 | 
						|
 | 
						|
        if err:
 | 
						|
            print(err.decode(), file=sys.stderr)
 | 
						|
 | 
						|
        if not out:
 | 
						|
            print(
 | 
						|
                f"Error generating metadata in {project_shortname} (status={status})",
 | 
						|
                file=sys.stderr,
 | 
						|
            )
 | 
						|
            sys.exit(1)
 | 
						|
 | 
						|
        metadata = safe_json_loads(out.decode("utf-8"))
 | 
						|
 | 
						|
    py_generator_kwargs = {
 | 
						|
        "custom_typing_module": custom_typing_module,
 | 
						|
    }
 | 
						|
    if keep_prop_order is not None:
 | 
						|
        keep_prop_order = [
 | 
						|
            component.strip(" ") for component in keep_prop_order.split(",")
 | 
						|
        ]
 | 
						|
        py_generator_kwargs["prop_reorder_exceptions"] = keep_prop_order
 | 
						|
 | 
						|
    if max_props:
 | 
						|
        py_generator_kwargs["max_props"] = max_props
 | 
						|
 | 
						|
    generator_methods = [functools.partial(generate_class_file, **py_generator_kwargs)]
 | 
						|
 | 
						|
    pkg_data = None
 | 
						|
    if rprefix is not None or jlprefix is not None:
 | 
						|
        with open("package.json", "r", encoding="utf-8") as f:
 | 
						|
            pkg_data = safe_json_loads(f.read())
 | 
						|
 | 
						|
    rpkg_data = None
 | 
						|
    if rprefix is not None:
 | 
						|
        if not os.path.exists("man"):
 | 
						|
            os.makedirs("man")
 | 
						|
        if not os.path.exists("R"):
 | 
						|
            os.makedirs("R")
 | 
						|
        if os.path.isfile("dash-info.yaml"):
 | 
						|
            with open("dash-info.yaml", encoding="utf-8") as yamldata:
 | 
						|
                rpkg_data = yaml.safe_load(yamldata)
 | 
						|
        generator_methods.append(
 | 
						|
            functools.partial(write_class_file, prefix=rprefix, rpkg_data=rpkg_data)
 | 
						|
        )
 | 
						|
 | 
						|
    if jlprefix is not None:
 | 
						|
        generator_methods.append(
 | 
						|
            functools.partial(generate_struct_file, prefix=jlprefix)
 | 
						|
        )
 | 
						|
 | 
						|
    components = generate_classes_files(project_shortname, metadata, *generator_methods)
 | 
						|
 | 
						|
    generate_prop_types(
 | 
						|
        metadata,
 | 
						|
        project_shortname,
 | 
						|
        custom_typing_module=custom_typing_module,
 | 
						|
    )
 | 
						|
 | 
						|
    with open(
 | 
						|
        os.path.join(project_shortname, "metadata.json"), "w", encoding="utf-8"
 | 
						|
    ) as f:
 | 
						|
        json.dump(metadata, f, separators=(",", ":"))
 | 
						|
 | 
						|
    generate_imports(project_shortname, components)
 | 
						|
 | 
						|
    if rprefix is not None:
 | 
						|
        generate_exports(
 | 
						|
            project_shortname,
 | 
						|
            components,
 | 
						|
            metadata,
 | 
						|
            pkg_data,
 | 
						|
            rpkg_data,
 | 
						|
            rprefix,
 | 
						|
            rdepends,
 | 
						|
            rimports,
 | 
						|
            rsuggests,
 | 
						|
        )
 | 
						|
 | 
						|
    if jlprefix is not None:
 | 
						|
        generate_module(project_shortname, components, metadata, pkg_data, jlprefix)
 | 
						|
 | 
						|
 | 
						|
def safe_json_loads(s):
 | 
						|
    jsondata_unicode = json.loads(s, object_pairs_hook=OrderedDict)
 | 
						|
    if sys.version_info[0] >= 3:
 | 
						|
        return jsondata_unicode
 | 
						|
    return byteify(jsondata_unicode)
 | 
						|
 | 
						|
 | 
						|
def component_build_arg_parser():
 | 
						|
    parser = argparse.ArgumentParser(
 | 
						|
        prog="dash-generate-components",
 | 
						|
        formatter_class=_CombinedFormatter,
 | 
						|
        description="Generate dash components by extracting the metadata "
 | 
						|
        "using react-docgen. Then map the metadata to Python classes.",
 | 
						|
    )
 | 
						|
    parser.add_argument("components_source", help="React components source directory.")
 | 
						|
    parser.add_argument(
 | 
						|
        "project_shortname", help="Name of the project to export the classes files."
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-p",
 | 
						|
        "--package-info-filename",
 | 
						|
        default="package.json",
 | 
						|
        help="The filename of the copied `package.json` to `project_shortname`",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-i",
 | 
						|
        "--ignore",
 | 
						|
        default="^_",
 | 
						|
        help="Files/directories matching the pattern will be ignored",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "--r-prefix",
 | 
						|
        help="Specify a prefix for Dash for R component names, write "
 | 
						|
        "components to R dir, create R package.",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "--r-depends",
 | 
						|
        default="",
 | 
						|
        help="Specify a comma-separated list of R packages to be "
 | 
						|
        "inserted into the Depends field of the DESCRIPTION file.",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "--r-imports",
 | 
						|
        default="",
 | 
						|
        help="Specify a comma-separated list of R packages to be "
 | 
						|
        "inserted into the Imports field of the DESCRIPTION file.",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "--r-suggests",
 | 
						|
        default="",
 | 
						|
        help="Specify a comma-separated list of R packages to be "
 | 
						|
        "inserted into the Suggests field of the DESCRIPTION file.",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "--jl-prefix",
 | 
						|
        help="Specify a prefix for Dash for R component names, write "
 | 
						|
        "components to R dir, create R package.",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-k",
 | 
						|
        "--keep-prop-order",
 | 
						|
        default=None,
 | 
						|
        help="Specify a comma-separated list of components which will use the prop "
 | 
						|
        "order described in the component proptypes instead of alphabetically reordered "
 | 
						|
        "props. Pass the 'ALL' keyword to have every component retain "
 | 
						|
        "its original prop order.",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "--max-props",
 | 
						|
        type=int,
 | 
						|
        default=250,
 | 
						|
        help="Specify the max number of props to list in the component signature. "
 | 
						|
        "More props will still be shown in the docstring, and will still work when "
 | 
						|
        "provided as kwargs to the component. Python <3.7 only supports 255 args, "
 | 
						|
        "but you may also want to reduce further for improved readability at the "
 | 
						|
        "expense of auto-completion for the later props. Use 0 to include all props.",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-t",
 | 
						|
        "--custom-typing-module",
 | 
						|
        type=str,
 | 
						|
        default="dash_prop_typing",
 | 
						|
        help=" Module containing custom typing definition for components."
 | 
						|
        "Can contains two variables:\n"
 | 
						|
        " - custom_imports: dict[ComponentName, list[str]].\n"
 | 
						|
        " - custom_props: dict[ComponentName, dict[PropName, function]].\n",
 | 
						|
    )
 | 
						|
    return parser
 | 
						|
 | 
						|
 | 
						|
def cli():
 | 
						|
    # Add current path for loading modules.
 | 
						|
    sys.path.insert(0, ".")
 | 
						|
    args = component_build_arg_parser().parse_args()
 | 
						|
    generate_components(
 | 
						|
        args.components_source,
 | 
						|
        args.project_shortname,
 | 
						|
        package_info_filename=args.package_info_filename,
 | 
						|
        ignore=args.ignore,
 | 
						|
        rprefix=args.r_prefix,
 | 
						|
        rdepends=args.r_depends,
 | 
						|
        rimports=args.r_imports,
 | 
						|
        rsuggests=args.r_suggests,
 | 
						|
        jlprefix=args.jl_prefix,
 | 
						|
        keep_prop_order=args.keep_prop_order,
 | 
						|
        max_props=args.max_props,
 | 
						|
        custom_typing_module=args.custom_typing_module,
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
# pylint: disable=undefined-variable
 | 
						|
def byteify(input_object):
 | 
						|
    if isinstance(input_object, dict):
 | 
						|
        return OrderedDict(
 | 
						|
            [(byteify(key), byteify(value)) for key, value in input_object.items()]
 | 
						|
        )
 | 
						|
    if isinstance(input_object, list):
 | 
						|
        return [byteify(element) for element in input_object]
 | 
						|
    if isinstance(input_object, str):  # noqa:F821
 | 
						|
        return input_object.encode(encoding="utf-8")
 | 
						|
    return input_object
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    cli()
 |