#!/usr/bin/env python
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: © 2021 Massachusetts Institute of Technology.
# SPDX-FileCopyrightText: © 2021 Lee McCuller <mcculler@caltech.edu>
# NOTICE: authors should document their contributions in concisely in NOTICE
# with details inline in source files, comments, and docstrings.
"""
TODO, needs big documentation
"""
import sys
import argparse
import collections
from ..overridable_object import OverridableObject
from ..utilities import SuperBase, NOARG
from ..properties import mproperty
TYPES = dict(
command="command",
group="group",
argument="argument",
)
[docs]
class OOArgParse(
OverridableObject,
SuperBase,
object,
):
@classmethod
def __cls_argparse__(
cls,
args=None,
_sys_exit=True,
__usage_prog__=None,
base_kwargs=dict(),
no_commands=False,
**exkwargs
):
arguments = dict()
commands = dict()
groups = dict()
for attr_name in dir(cls):
attr_obj = getattr(cls, attr_name)
arganno = getattr(attr_obj, "_argparse", None)
if arganno:
if arganno.type == TYPES['argument']:
# mask the argument if it was specified in an extra kwarg
if attr_name not in exkwargs:
arguments[attr_name] = arganno
elif arganno.type == TYPES['group']:
groups[attr_name] = arganno
elif arganno.type == TYPES['command']:
commands[attr_name] = arganno
description = getattr(cls, "__arg_desc__", cls.__doc__)
ap = argparse.ArgumentParser(
prog=__usage_prog__, description=description, add_help=False
)
ap.add_argument(
"-h",
"--help",
"--halp!",
dest="help",
action="store_true",
help="Show this help screen"
# help = argparse.SUPPRESS,
)
# build groups
glist = collections.deque(sorted([(v.order, k, v) for k, v in groups.items()]))
gengroups = dict()
gengroups[None] = ap
while glist:
order, name, gbunch = glist.pop()
useparse = gengroups.get(gbunch.group, None)
if useparse is None:
if gbunch.group not in groups:
raise RuntimeError("Group {0} not defined!".format(gbunch.group))
# reinsert in back
glist.appendleft((name, gbunch))
continue
group = gbunch.func(useparse)
gengroups[name] = group
default_cmd = getattr(cls, "__arg_default__", None)
commands_byname = dict()
for name, cbunch in commands.items():
commands_byname[cbunch.cmd_name] = cbunch
cmdlist = list(
k for o, k in sorted((v.order, k) for k, v in commands_byname.items())
)
if not no_commands:
if default_cmd is None:
ap.add_argument(
"__command__",
metavar="COMMAND",
choices=cmdlist,
help=argparse.SUPPRESS,
)
else:
if commands_byname:
ap.add_argument(
"__command__",
metavar="COMMAND",
choices=cmdlist + [""],
default=None,
nargs="?",
help=argparse.SUPPRESS,
)
if len(commands_byname) > 0:
ap.add_argument(
"__args__",
metavar="args",
nargs=argparse.REMAINDER,
help=argparse.SUPPRESS,
)
argdo = dict()
for order, argname, argbunch in sorted(
[(v.order, k, v) for k, v in arguments.items()]
):
useparse = gengroups.get(argbunch.group, None)
argdo[argname] = argbunch.func(useparse)
if args is None:
args = sys.argv[1:]
print_help = False
# TODO fix -h handling requiring otherwise perfect other arguments
if "-h" in args or "--help" in args or "--halp!" in args:
try:
class BooBoo(Exception):
pass
def error(message):
raise BooBoo(message)
ap.error = error
args_parsed = ap.parse_args(args)
print_help = args_parsed.help
except BooBoo:
print_help = True
else:
args_parsed = ap.parse_args(args)
print_help = args_parsed.help
if print_help:
spp = ap.add_subparsers(
dest="command",
help="(May have subarguments, use -h after them for command specific help)",
)
if default_cmd is not None:
if default_cmd.__doc__ is not None:
doc = default_cmd.__doc__ + " [Default, no arguments]"
else:
doc = "Default Action [command only]"
spp.add_parser(
'""',
help=doc,
)
if not no_commands:
for name, cbunch in commands.items():
commands_byname[cbunch.cmd_name] = cbunch
if cbunch.takes_arguments:
extra = " [takes arguments]"
else:
extra = " [command only]"
spp.add_parser(
cbunch.cmd_name,
help=cbunch.description + extra,
)
ap.print_help()
sys.exit(0)
kwargs = dict(base_kwargs)
for argname, argbunch in arguments.items():
val = getattr(args_parsed, argname)
if val is not NOARG:
kwargs[argbunch.name_inject] = val
if commands_byname and not no_commands:
command = getattr(args_parsed, "__command__", NOARG)
if command is not None:
command_idx_in_args = args.index(command)
call_args = args[:command_idx_in_args]
else:
call_args = args[:]
else:
command = None
call_args = args
args_sub = getattr(args_parsed, "__args__", NOARG)
# add in the extra kwargs that were given
kwargs.update(exkwargs)
kwargs["__cls_argparse_args__"] = tuple(call_args)
kwargs["__cls_argparse_cmd__"] = command
kwargs["__cls_argparse_cmd_args__"] = (
tuple(args_sub) if args_sub is not NOARG else None
)
obj = cls(**kwargs)
if no_commands:
return obj
if command is None:
ret = obj.__arg_default__()
else:
cbunch = commands_byname[command]
call = getattr(obj, cbunch.run_name)
# TODO, check about taking additional arguments
if not cbunch.takes_arguments and args_sub:
raise RuntimeError("Final Command does not take additional arguments")
ret = call(args_sub)
if _sys_exit:
sys.exit(ret)
return ret
@mproperty
def __cls_argparse_args__(self, args=None):
return args
@mproperty
def __cls_argparse_cmd__(self, args=None):
return args
@mproperty
def __cls_argparse_cmd_args__(self, args=None):
return args