275 lines
8.8 KiB
Python
275 lines
8.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Translate an options data structure into command line args
|
|
"""
|
|
|
|
# Import python libs
|
|
import sys
|
|
import inspect
|
|
import argparse
|
|
import functools
|
|
import collections
|
|
import pop.hub
|
|
|
|
__virtualname__ = "args"
|
|
__contracts__ = [__virtualname__]
|
|
|
|
|
|
class ActionWrapper:
|
|
"""
|
|
This class wraps argparse.Action instances in order to mark arguments passed
|
|
on CLI as explicitly passed
|
|
"""
|
|
|
|
def __init__(self, action):
|
|
self._action = action
|
|
functools.update_wrapper(self, action)
|
|
|
|
def __call__(self, parser, namespace, values, option_string):
|
|
# Let's store the call to this option as an explicit CLI call for later
|
|
# use when overwriting any configuration settings on file with those
|
|
# from CLI
|
|
if getattr(parser, "_explicit_cli_args_", None) is None:
|
|
setattr(parser, "_explicit_cli_args_", set())
|
|
parser._explicit_cli_args_.add(
|
|
self._action.dest
|
|
) # pylint: disable=protected-access
|
|
# Carry on regular operation
|
|
return self._action(parser, namespace, values, option_string)
|
|
|
|
def __getattribute__(self, name):
|
|
if name == "_action":
|
|
return object.__getattribute__(self, name)
|
|
# Proxy any attribute's search to the _action instance
|
|
return getattr(self._action, name)
|
|
|
|
def __repr__(self):
|
|
return repr(self._action)
|
|
|
|
|
|
class ActionClassWrapper:
|
|
"""
|
|
This class wraps argparse.Action classes in order to mark arguments passed
|
|
on CLI as explicitly passed
|
|
"""
|
|
|
|
def __init__(self, klass):
|
|
self._klass = klass
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
return ActionWrapper(self._klass(*args, **kwargs))
|
|
|
|
def __repr__(self):
|
|
return repr(self._klass)
|
|
|
|
def __getattribute__(self, name):
|
|
if name == "_klass":
|
|
return object.__getattribute__(self, name)
|
|
# Proxy any attributes search to the _klass instance
|
|
return getattr(self._klass, name)
|
|
|
|
|
|
class ArgumentParser(argparse.ArgumentParser):
|
|
def register(self, name, value, obj): # pylint: disable=arguments-differ
|
|
if name == "action":
|
|
# Let's wrap it on our action class wrapper so we can latter store
|
|
# which options were explicitly passed from CLI
|
|
return super(ArgumentParser, self).register(
|
|
name, value, ActionClassWrapper(obj)
|
|
)
|
|
return super(ArgumentParser, self).register(name, value, obj)
|
|
|
|
def parse_known_args(self, args=None, namespace=None):
|
|
namespace, arg_strings = super().parse_known_args(args, namespace)
|
|
explicit_cli_args = getattr(self, "_explicit_cli_args_", set())
|
|
if "_explicit_cli_args_" not in namespace:
|
|
setattr(namespace, "_explicit_cli_args_", set())
|
|
namespace._explicit_cli_args_.update(explicit_cli_args)
|
|
return namespace, arg_strings
|
|
|
|
|
|
def __init__(hub: "pop.hub.Hub"):
|
|
"""
|
|
Set up the local memory copy of the parser
|
|
"""
|
|
hub.conf._mem["args"] = {}
|
|
|
|
|
|
def _init_parser(hub: "pop.hub.Hub", opts):
|
|
if "parser" not in hub.conf._mem["args"]:
|
|
# Instantiate the parser
|
|
hub.conf._mem["args"]["parser"] = ArgumentParser(**opts.get("_argparser_", {}))
|
|
|
|
|
|
def _keys(opts):
|
|
"""
|
|
Return the keys in the right order
|
|
"""
|
|
if isinstance(opts, collections.OrderedDict):
|
|
return sorted(
|
|
list(opts), key=lambda k: opts[k].get("display_priority", sys.maxsize)
|
|
)
|
|
return sorted(opts, key=lambda k: (opts[k].get("display_priority", sys.maxsize), k))
|
|
|
|
|
|
def subs(hub: "pop.hub.Hub", opts):
|
|
"""
|
|
Set up sub parsers, if using sub parsers this needs to be called
|
|
before calling setup.
|
|
|
|
opts dict:
|
|
<sub_title>:
|
|
[desc]: 'Some subparser'
|
|
help: 'subparser!'
|
|
"""
|
|
_init_parser(hub, opts)
|
|
hub.conf._mem["args"]["sub"] = hub.conf._mem["args"]["parser"].add_subparsers(
|
|
dest="_subparser_"
|
|
)
|
|
hub.conf._mem["args"]["subs"] = {}
|
|
for arg in _keys(opts):
|
|
if arg in ("_argparser_",):
|
|
continue
|
|
comps = opts[arg]
|
|
kwargs = {}
|
|
if "help" in comps:
|
|
kwargs["help"] = comps["help"]
|
|
if "desc" in comps:
|
|
kwargs["description"] = comps["desc"]
|
|
hub.conf._mem["args"]["subs"][arg] = hub.conf._mem["args"]["sub"].add_parser(
|
|
arg, **kwargs
|
|
)
|
|
return {"result": True, "return": True}
|
|
|
|
|
|
def setup(hub: "pop.hub.Hub", opts):
|
|
"""
|
|
Take in a pre-defined opts dict and translate it to args
|
|
|
|
opts dict:
|
|
<arg>:
|
|
[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
|
|
"""
|
|
_init_parser(hub, opts)
|
|
defaults = {}
|
|
groups = {}
|
|
ex_groups = {}
|
|
for arg in _keys(opts):
|
|
if arg in ("_argparser_",):
|
|
continue
|
|
comps = opts[arg]
|
|
positional = comps.pop("positional", False)
|
|
if positional:
|
|
args = [arg]
|
|
else:
|
|
long_opts = ["--{}".format(arg.replace("_", "-"))]
|
|
short_opts = []
|
|
for o_str in comps.get("options", []):
|
|
if not o_str.startswith("--") and o_str.startswith("-"):
|
|
short_opts.append(o_str)
|
|
continue
|
|
long_opts.append(o_str)
|
|
args = short_opts + long_opts
|
|
kwargs = {}
|
|
kwargs["action"] = action = comps.get("action", None)
|
|
|
|
if action is None:
|
|
# Non existing option defaults to a StoreAction in argparse
|
|
action = hub.conf._mem["args"]["parser"]._registry_get(
|
|
"action", action
|
|
) # pylint: disable=protected-access
|
|
|
|
if isinstance(action, str):
|
|
signature = inspect.signature(
|
|
hub.conf._mem["args"]["parser"]._registry_get("action", action).__init__
|
|
) # pylint: disable=protected-access
|
|
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.conf._mem["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.conf._mem["args"][
|
|
"parser"
|
|
].add_mutually_exclusive_group()
|
|
ex_groups[group].add_argument(*args, **kwargs)
|
|
continue
|
|
if "sub" in comps:
|
|
subs = comps["sub"]
|
|
if not isinstance(subs, list):
|
|
subs = [subs]
|
|
for sub in subs:
|
|
sparse = hub.conf._mem["args"]["subs"].get(sub)
|
|
if not sparse:
|
|
# Maybe raise exception here? Malformed config?
|
|
continue
|
|
sparse.add_argument(*args, **kwargs)
|
|
continue
|
|
hub.conf._mem["args"]["parser"].add_argument(*args, **kwargs)
|
|
return {"result": True, "return": defaults}
|
|
|
|
|
|
def parse(
|
|
hub: "pop.hub.Hub", args=None, namespace=None, only_parse_known_arguments=False
|
|
):
|
|
"""
|
|
Parse the command line options
|
|
"""
|
|
if only_parse_known_arguments:
|
|
opts, unknown_args = hub.conf._mem["args"]["parser"].parse_known_args(
|
|
args, namespace
|
|
)
|
|
opts_dict = opts.__dict__
|
|
opts_dict["_unknown_args_"] = unknown_args
|
|
else:
|
|
opts = hub.conf._mem["args"]["parser"].parse_args(args, namespace)
|
|
opts_dict = opts.__dict__
|
|
return {"result": True, "return": opts_dict}
|
|
|
|
|
|
def render(hub: "pop.hub.Hub", defaults, cli_opts, explicit_cli_args):
|
|
"""
|
|
For options specified as such, take the string passed into the cli and
|
|
render it using the specified render flag
|
|
"""
|
|
for key in explicit_cli_args:
|
|
rend = defaults.get(key, {}).get("render")
|
|
if rend:
|
|
ref = f"conf.{rend}.render"
|
|
cli_opts[key] = hub.pop.ref.last(ref)(cli_opts[key])
|
|
return cli_opts
|