"""
The scriptconfig ModalCLI
This module defines a way to group several smaller scriptconfig CLIs into a
single parent CLI that chooses between them "modally". E.g. if we define two
configs: do_foo and do_bar, we use ModalCLI to define a parent program that can run
one or the other. Let's make this more concrete.
CommandLine:
xdoctest -m scriptconfig.modal __doc__:0
Example
>>> import scriptconfig as scfg
>>> #
>>> class DoFooCLI(scfg.DataConfig):
>>> __command__ = 'do_foo'
>>> option1 = scfg.Value(None, help='option1')
>>> #
>>> @classmethod
>>> def main(cls, cmdline=1, **kwargs):
>>> self = cls.cli(cmdline=cmdline, data=kwargs)
>>> print('Called Foo with: ' + str(self))
>>> #
>>> class DoBarCLI(scfg.DataConfig):
>>> __command__ = 'do_bar'
>>> option1 = scfg.Value(None, help='option1')
>>> #
>>> @classmethod
>>> def main(cls, cmdline=1, **kwargs):
>>> self = cls.cli(cmdline=cmdline, data=kwargs)
>>> print('Called Bar with: ' + str(self))
>>> #
>>> #
>>> class MyModalCLI(scfg.ModalCLI):
>>> __version__ = '1.2.3'
>>> foo = DoFooCLI
>>> bar = DoBarCLI
>>> #
>>> modal = MyModalCLI()
>>> MyModalCLI.main(argv=['do_foo'])
>>> #MyModalCLI.main(argv=['do-foo'])
>>> MyModalCLI.main(argv=['--version'])
>>> try:
>>> MyModalCLI.main(argv=['--help'])
>>> except SystemExit:
>>> print('prevent system exit due to calling --help')
"""
import ubelt as ub
from scriptconfig.util.util_class import class_or_instancemethod
# from scriptconfig.config import MetaConfig
DEFAULT_GROUP = 'commands'
[docs]
class ModalCLI(metaclass=MetaModalCLI):
"""
Contains multiple scriptconfig.Config items with corresponding `main`
functions.
CommandLine:
xdoctest -m scriptconfig.modal ModalCLI
Example:
>>> from scriptconfig.modal import * # NOQA
>>> import scriptconfig as scfg
>>> self = ModalCLI(description='A modal CLI')
>>> #
>>> @self.register
>>> class Command1Config(scfg.Config):
>>> __command__ = 'command1'
>>> __default__ = {
>>> 'foo': 'spam'
>>> }
>>> @classmethod
>>> def main(cls, cmdline=1, **kwargs):
>>> config = cls(cmdline=cmdline, data=kwargs)
>>> print('config1 = {}'.format(ub.urepr(dict(config), nl=1)))
>>> #
>>> @self.register
>>> class Command2Config(scfg.DataConfig):
>>> __command__ = 'command2'
>>> foo = 'eggs'
>>> baz = 'biz'
>>> @classmethod
>>> def main(cls, cmdline=1, **kwargs):
>>> config = cls.cli(cmdline=cmdline, data=kwargs)
>>> print('config2 = {}'.format(ub.urepr(dict(config), nl=1)))
>>> #
>>> parser = self.argparse()
>>> parser.print_help()
...
A modal CLI
...
commands:
{command1,command2} specify a command to run
command1 argparse CLI generated by scriptconfig...
command2 argparse CLI generated by scriptconfig...
>>> self.run(argv=['command1'])
config1 = {
'foo': 'spam',
}
>>> self.run(argv=['command2', '--baz=buz'])
config2 = {
'foo': 'eggs',
'baz': 'buz',
}
CommandLine:
xdoctest -m scriptconfig.modal ModalCLI:1
Example:
>>> # Declarative modal CLI (new in 0.7.9)
>>> import scriptconfig as scfg
>>> class MyModalCLI(scfg.ModalCLI):
>>> #
>>> class Command1(scfg.DataConfig):
>>> __command__ = 'command1'
>>> foo = scfg.Value('spam', help='spam spam spam spam')
>>> @classmethod
>>> def main(cls, cmdline=1, **kwargs):
>>> config = cls.cli(cmdline=cmdline, data=kwargs)
>>> print('config1 = {}'.format(ub.urepr(dict(config), nl=1)))
>>> #
>>> class Command2(scfg.DataConfig):
>>> __command__ = 'command2'
>>> foo = 'eggs'
>>> baz = 'biz'
>>> @classmethod
>>> def main(cls, cmdline=1, **kwargs):
>>> config = cls.cli(cmdline=cmdline, data=kwargs)
>>> print('config2 = {}'.format(ub.urepr(dict(config), nl=1)))
>>> #
>>> MyModalCLI.main(argv=['command1'])
>>> MyModalCLI.main(argv=['command2', '--baz=buz'])
Example:
>>> # Declarative modal CLI (new in 0.7.9)
>>> import scriptconfig as scfg
>>> class MyModalCLI(scfg.ModalCLI):
>>> ...
>>> #
>>> @MyModalCLI.register
>>> class Command1(scfg.DataConfig):
>>> __command__ = 'command1'
>>> foo = scfg.Value('spam', help='spam spam spam spam')
>>> @classmethod
>>> def main(cls, cmdline=1, **kwargs):
>>> config = cls.cli(cmdline=cmdline, data=kwargs)
>>> print('config1 = {}'.format(ub.urepr(dict(config), nl=1)))
>>> #
>>> @MyModalCLI.register
>>> class Command2(scfg.DataConfig):
>>> __command__ = 'command2'
>>> foo = 'eggs'
>>> baz = 'biz'
>>> @classmethod
>>> def main(cls, cmdline=1, **kwargs):
>>> config = cls.cli(cmdline=cmdline, data=kwargs)
>>> print('config2 = {}'.format(ub.urepr(dict(config), nl=1)))
>>> #
>>> MyModalCLI.main(argv=['command1'])
>>> MyModalCLI.main(argv=['command2', '--baz=buz'])
"""
__subconfigs__ = []
def __init__(self, description='', sub_clis=None, version=None):
if sub_clis is None:
sub_clis = []
if self.__class__.__name__ != 'ModalCLI':
self.description = description or ub.codeblock(self.__doc__ or '')
else:
self.description = description
self._instance_subconfigs = sub_clis + self.__subconfigs__
if version is None:
version = getattr(self.__class__, '__version__', None)
if version is None:
version = getattr(self.__class__, 'version', None)
self.version = version
def __call__(self, cli_cls):
""" alias of register """
return self.register(cli_cls)
@property
def sub_clis(self):
# backwards compat
return self._instance_subconfigs
[docs]
@class_or_instancemethod
def register(cls_or_self, cli_cls):
"""
Args:
cli_cli (scriptconfig.Config):
A CLI-aware config object to register as a sub CLI
"""
# Note: the order or registration is how it will appear in the CLI help
# Hack for older scriptconfig
# if not hasattr(cli_cls, 'default'):
# cli_cls.default = cli_cls.__default__
if isinstance(cls_or_self, type):
# Called as a class method
cls_or_self.__subconfigs__.append(cli_cls)
else:
# Called as an instance method
cls_or_self._instance_subconfigs.append(cli_cls)
return cli_cls
[docs]
def _build_subcmd_infos(self):
cmdinfo_list = []
for cli_cls in self.sub_clis:
cmdname = getattr(cli_cls, '__command__', None)
if cmdname is None:
cmdname = cli_cls.__name__
if cmdname is None:
raise ValueError(ub.paragraph(
f'''
The ModalCLI expects that registered subconfigs have a
``__command__: str`` attribute, but {cli_cls} is missing one.
'''))
if not hasattr(cli_cls, 'main'):
raise ValueError(ub.paragraph(
f'''
The ModalCLI expects that registered subconfigs have a
``main`` classmethod with the signature
``main(cls, cmdline: bool, **kwargs)``,
but {cli_cls} is missing one.
'''))
parserkw = {}
__alias__ = getattr(cli_cls, '__alias__', [])
if __alias__:
parserkw['aliases'] = __alias__
group = getattr(cli_cls, '__group__', DEFAULT_GROUP)
# group = 'FOO'
# print(f'cli_cls={cli_cls}')
# print(isinstance(cli_cls, ModalCLI))
# print('cli_cls.__bases__ = {}'.format(ub.urepr(cli_cls.__bases__, nl=1)))
# print('ModalCLI = {}'.format(ub.urepr(ModalCLI, nl=1)))
if isinstance(cli_cls, ModalCLI) or issubclass(cli_cls, ModalCLI):
# Another modal layer
modal = cli_cls()
parserkw.update(modal._parserkw())
parserkw['help'] = parserkw['description'].split('\n')[0]
cmdinfo_list.append({
'is_modal': True,
'cmdname': cmdname,
'parserkw': parserkw,
'main_func': cli_cls.main,
'subconfig': modal,
'group': group,
})
else:
# A leaf Config CLI
subconfig = cli_cls()
parserkw.update(subconfig._parserkw())
parserkw['help'] = parserkw['description'].split('\n')[0]
cmdinfo_list.append({
'is_modal': False,
'cmdname': cmdname,
'parserkw': parserkw,
'main_func': cli_cls.main,
'subconfig': subconfig,
'group': group,
})
return cmdinfo_list
[docs]
def _parserkw(self):
"""
Generate the kwargs for making a new argparse.ArgumentParser
"""
from scriptconfig.argparse_ext import RawDescriptionDefaultsHelpFormatter
parserkw = dict(
description=self.description,
formatter_class=RawDescriptionDefaultsHelpFormatter,
epilog=getattr(self, '__epilog__', None),
prog=getattr(self, '__prog__', None),
)
if hasattr(self, '__allow_abbrev__'):
parserkw['allow_abbrev'] = self.__allow_abbrev__
return parserkw
[docs]
def argparse(self, parser=None, special_options=...):
"""
Builds a new argparse object for this ModalCLI or extends an existing
one with it.
"""
if parser is None:
import argparse as argparse_mod
parserkw = self._parserkw()
parser = argparse_mod.ArgumentParser(**parserkw)
if hasattr(self, 'version') and self.version is not None:
parser.add_argument('--version', action='store_true',
help='show version number and exit')
# Prepare information to be added to the subparser before it is created
cmdinfo_list = self._build_subcmd_infos()
# Build a list of primary command names to display as the valid options
# for subparsers. This avoids cluttering the screen with all aliases
# which happens by default.
# The subparser is what enables the modal CLI. It will redirect a
# command to a chosen subparser.
# group_to_cmdinfos = ub.group_items(cmdinfo_list, key=lambda x: x['group'])
# TODO: groups?
# https://stackoverflow.com/questions/32017020/grouping-argparse-subparser-arguments
_command_choices = [d['cmdname'] for d in cmdinfo_list]
_metavar = '{' + ','.join(_command_choices) + '}'
command_subparsers = parser.add_subparsers(
title='commands', help='specify a command to run', metavar=_metavar)
# group_to_subparser = {}
# for group, cmdinfos in group_to_cmdinfos.items():
# ...
def fuzzy_cmd_names(n):
options = []
options.append(n)
v1 = n.replace('-', '_')
if v1 not in options:
options.append(v1)
v2 = n.replace('_', '-')
if v2 not in options:
options.append(v2)
main_cmd, *aliases = options
return main_cmd, aliases
for cmdinfo in cmdinfo_list:
# group = cmdinfo['group']
# Add a new command to subparser_group
main_cmd, aliases = fuzzy_cmd_names(cmdinfo['cmdname'])
# TODO: enable alternate hyphen/underscore aliases, but supress
# them from the help output. Even better would be to handle
# argument completion so they aren't clobbered.
aliases = []
parserkw = cmdinfo['parserkw']
if 'aliases' in parserkw:
parserkw['aliases'] = list(parserkw['aliases']) + list(aliases)
else:
if aliases:
parserkw['aliases'] = aliases
if cmdinfo['is_modal']:
modal_inst = cmdinfo['subconfig']
modal_parser = command_subparsers.add_parser(
main_cmd, **parserkw)
modal_parser = modal_inst.argparse(parser=modal_parser)
modal_parser.set_defaults(main=cmdinfo['main_func'])
else:
subparser = command_subparsers.add_parser(
main_cmd, **parserkw)
subparser = cmdinfo['subconfig'].argparse(subparser)
subparser.set_defaults(main=cmdinfo['main_func'])
return parser
build_parser = argparse
[docs]
@class_or_instancemethod
def main(self, argv=None, strict=True, autocomplete='auto'):
"""
Execute the modal CLI as the main script
"""
if isinstance(self, type):
self = self()
parser = self.argparse()
if autocomplete:
try:
import argcomplete
# Need to run: "$(register-python-argcomplete xdev)"
# or activate-global-python-argcomplete --dest=-
# activate-global-python-argcomplete --dest ~/.bash_completion.d
# To enable this.
except ImportError:
argcomplete = None
if autocomplete != 'auto':
raise
else:
argcomplete = None
if argcomplete is not None:
argcomplete.autocomplete(parser)
if strict:
ns = parser.parse_args(args=argv)
else:
ns, _ = parser.parse_known_args(args=argv)
kw = ns.__dict__
if kw.pop('version', None):
print(self.version)
return 0
sub_main = kw.pop('main', None)
if sub_main is None:
parser.print_help()
raise ValueError('no command given')
return 1
try:
ret = sub_main(cmdline=False, **kw)
except Exception as ex:
print('ERROR ex = {!r}'.format(ex))
raise
return 1
else:
if ret is None:
ret = 0
return ret
run = main # alias for backwards compatiability