# -*- coding: utf-8 -*- """ Translate an options data structure into command line args """ # Import python libs import sys import inspect import argparse import dict_tools.update from typing import Any, Dict, List, Tuple def __init__(hub): hub.config.args.DEFAULT = object() def _keys(opts): """ Return the keys in the right order """ return sorted(opts, key=lambda k: (opts[k].get("display_priority", sys.maxsize), k)) def gather( hub, raw: Dict[str, Any], cli: str, parse_cli: bool ) -> Tuple[Dict[str, Any], Dict[str, Any]]: """ Return the cli arguments as they are parsed """ if not parse_cli: return {}, {} raw_cli = hub.config.args.get_cli(raw, cli) hub.config.args.init_parser() hub.config.args.subparsers(raw, cli) hub.config.args.setup(raw_cli) cli_args = hub.config.args.parse() cli_args = hub.config.args.render(cli_args, raw_cli) cli_args = hub.config.args.clean_defaults(cli_args) return cli_args, raw_cli def clean_defaults(hub, cli_args: Dict[str, Any]) -> Dict[str, Any]: """ If anyone did not pass in an argument then the key will match the bad default and needs to be removed """ ret = {} for key, val in cli_args.items(): if val is not hub.config.args.DEFAULT: ret[key] = val return ret def init_parser(hub): if "parser" not in hub.config.ARGS: # Instantiate the parser hub.config.ARGS["parser"] = argparse.ArgumentParser() def get_cli(hub, raw: Dict[str, Any], cli: str) -> Dict[str, Any]: """ Gather the arguments that need to be parsed by the CLI """ ret = {} main = raw.get(cli, {}).get("CLI_CONFIG") main_raw = raw.get(cli, {}).get("CONFIG") for key, data in main.items(): ret[key] = {} dict_tools.update.update(ret[key], data) if key in main_raw: dict_tools.update.update(ret[key], main_raw[key]) if "source" in data: src = raw.get(data["source"], {}).get("CONFIG", {}).get(key) if src is not None: dict_tools.update.update(ret[key], src) if "default" in ret[key]: ret[key]["default"] = hub.config.args.DEFAULT ret.update(hub.config.version.CONFIG) return ret def subparsers(hub, raw: Dict[str, Any], cli: str) -> bool: """ Look over the data and extract and set up the subparsers for subcommands """ subs = raw.get(cli, {}).get("SUBCOMMANDS") if not subs: return True hub.config.ARGS["sub"] = hub.config.ARGS["parser"].add_subparsers( dest="_subparser_" ) hub.config.ARGS["subs"] = {} for arg in _keys(subs): if arg in ("_argparser_",): continue comps = subs[arg] kwargs = {} if "help" in comps: kwargs["help"] = comps["help"] if "desc" in comps: kwargs["description"] = comps["desc"] hub.config.ARGS["subs"][arg] = hub.config.ARGS["sub"].add_parser(arg, **kwargs) return True def setup(hub, raw_cli: Dict[str, Any]) -> Dict[str, Any]: """ Take in a pre-defined dict and translate it to args opts dict: : [group]: foo [default]: bar [action]: store_true [options]: # arg will be turned into --arg - '-A' - '--cheese' [choices]: - foo - bar - baz [nargs]: + [type]: int [dest]: cheese help: Some great help message """ # TODO: This should be broken up defaults = {} groups = {} ex_groups = {} for arg in _keys(raw_cli): if arg in ("_argparser_",): continue comps = raw_cli[arg] positional = comps.pop("positional", False) if positional: args = [arg] else: args = [f"--{arg.replace('_', '-')}"] for o_str in comps.get("options", ()): if len(o_str) == 1: o_str = f"-{o_str}" elif not o_str.startswith("-"): o_str = f"--{o_str}" if o_str not in args: args.append(o_str) kwargs = {} kwargs["action"] = action = comps.get("action", None) if action is None: # Non existing option defaults to a StoreAction in argparse action = hub.config.ARGS["parser"]._registry_get("action", action) if isinstance(action, str): signature = inspect.signature( hub.config.ARGS["parser"]._registry_get("action", action).__init__ ) else: signature = inspect.signature(action.__init__) for param in signature.parameters: if param == "self" or param not in comps: continue if param == "dest": kwargs["dest"] = comps.get("dest", arg) continue if param == "help": kwargs["help"] = comps.get("help", "THIS NEEDS SOME DOCUMENTATION!!") continue if param == "default": defaults[comps.get("dest", arg)] = comps[param] kwargs[param] = comps[param] if "group" in comps: group = comps["group"] if group not in groups: groups[group] = hub.config.ARGS["parser"].add_argument_group(group) groups[group].add_argument(*args, **kwargs) continue if "ex_group" in comps: group = comps["ex_group"] if group not in ex_groups: ex_groups[group] = hub.config.ARGS[ "parser" ].add_mutually_exclusive_group() ex_groups[group].add_argument(*args, **kwargs) continue if "subcommands" in comps: subs = comps["subcommands"] if not isinstance(subs, list): subs = [subs] for sub in subs: if sub == "_global_": if "subs" not in hub.config.ARGS: continue hub.config.ARGS["parser"].add_argument(*args, **kwargs) for named, sparse in hub.config.ARGS["subs"].items(): sparse.add_argument(*args, **kwargs) continue sparse = hub.config.ARGS.get("subs", {}).get(sub) if not sparse: # Maybe raise exception here? Malformed config? continue sparse.add_argument(*args, **kwargs) continue hub.config.ARGS["parser"].add_argument(*args, **kwargs) return defaults def parse( hub, args: List[str] = None, namespace: argparse.Namespace = None, only_parse_known_arguments: bool = False, ) -> Dict[str, Any]: """ Parse the command line options """ if only_parse_known_arguments: opts, unknown_args = hub.config.ARGS["parser"].parse_known_args(args, namespace) opts_dict = opts.__dict__ opts_dict["_unknown_args_"] = unknown_args else: opts = hub.config.ARGS["parser"].parse_args(args, namespace) opts_dict = opts.__dict__ hub.SUBPARSER = opts_dict.get("_subparser_", None) return opts_dict def render(hub, cli_args: Dict[str, Any], raw_cli: Dict[str, Any]) -> Dict[str, Any]: """ For options specified as such, take the string passed into the cli and render it using the specified render flag """ for key, val in raw_cli.items(): if key not in cli_args: continue if "render" not in val: continue if val["default"] != cli_args[key]: # The value was changed, render it cli_args[key] = hub.config.render.init.process(val["render"], cli_args[key]) return cli_args