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