Commit 0d249b3e authored by Stavros Sachtouris's avatar Stavros Sachtouris

Allow clis to overide command load implementation

Still buggy and experimental, but if a cli don't use the command
decorator, but implement another way of loading class info to
a _commands list of CommandTrees, kamaki can still use this cli.

This will allow clis to extent CommandTrees in order to provide
commands and command informationon demmand (dynamically)
parent 78496d42
......@@ -278,6 +278,7 @@ def parse_known_args(parser, arguments=None):
for name, arg in arguments.items():
arg.value = getattr(parsed, name, arg.default)
return parsed, unparsed
# ['"%s"' % s if ' ' in s else s for s in unparsed]
def init_parser(exe, arguments):
......
......@@ -155,6 +155,9 @@ class CommandTree(object):
if description is not None:
cmd.help = description
def has_command(self, path):
return path in self._all_commands
def get_command(self, path):
return self._all_commands[path]
......
......@@ -31,10 +31,10 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.command
#from kamaki.cli import command
#from kamaki.cli.new import command
from kamaki.cli.commands import _command_init
from kamaki.cli.command_tree import CommandTree
from kamaki.cli.argument import FlagArgument
#API_DESCRIPTION = dict(test='Test sample')
......@@ -44,20 +44,56 @@ _commands = [
]
def command(cmd_tree_list, prefix='', descedants_depth=1):
"""Load a class as a command
spec_cmd0_cmd1 will be command spec cmd0
@cmd_tree_list is initialized in cmd_spec file and is the structure
where commands are loaded. Var name should be _commands
@param prefix if given, load only commands prefixed with prefix,
@param descedants_depth is the depth of the tree descedants of the
prefix command. It is used ONLY if prefix and if prefix is not
a terminal command
"""
def wrap(cls):
cls_name = cls.__name__
spec = cls_name.split('_')[0]
cmd_tree = _commands[0] if spec == 'sample' else _commands[1]
if not cmd_tree:
return cls
cls.description, sep, cls.long_description\
= cls.__doc__.partition('\n')
from kamaki.cli.new import _construct_command_syntax
_construct_command_syntax(cls)
cmd_tree.add_command(cls_name, cls.description, cls)
return cls
return wrap
class _test_init(_command_init):
def main(self, *args, **kwargs):
print(self.__class__)
for v in args:
print('\t\targ: %s' % v)
for k, v in kwargs:
print('\t\tkwarg: %s: %s' % (k, v))
#@command()
@command(cmd_tree_list=_commands)
class sample_cmd0(_test_init):
""" test cmd"""
""" test cmd
This is the zero command test and this is the long description of it
"""
def main(self, mant):
super(self.__class__, self).main()
super(self.__class__, self).main(mant)
#@command()
@command(cmd_tree_list=_commands)
class sample_cmd_all(_test_init):
"""test cmd all"""
......@@ -65,30 +101,45 @@ class sample_cmd_all(_test_init):
super(self.__class__, self).main()
#@command()
@command(cmd_tree_list=_commands)
class sample_cmd_some(_test_init):
"""test_cmd_some"""
def main(self, opt='lala'):
super(self.__class__, self).main()
super(self.__class__, self).main(opt=opt)
@command(cmd_tree_list=_commands)
class test_cmd0(_test_init):
""" test cmd"""
def main(self, mant):
super(self.__class__, self).main()
super(self.__class__, self).main(mant)
@command(cmd_tree_list=_commands)
class test_cmd_all(_test_init):
"""test cmd all"""
def __init__(self, arguments={}):
super(self.__class__, self).__init__(arguments)
self.arguments['testarg'] = FlagArgument('a test arg', '--test')
def main(self):
super(self.__class__, self).main()
class test_cmd_some(_test_init):
@command(cmd_tree_list=_commands)
class test_cmdion(_test_init):
"""test_cmd_some"""
def main(self, opt='lala'):
super(self.__class__, self).main()
super(self.__class__, self).main(opt=opt)
@command(cmd_tree_list=_commands)
class test_cmd_cmdion_comedian(_test_init):
"""test_cmd_some"""
def main(self, opt='lala'):
super(self.__class__, self).main(opt=opt)
......@@ -31,17 +31,130 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.command
from sys import argv
from sys import argv, exit, stdout
from os.path import basename
from kamaki.cli.argument import _arguments, parse_known_args, init_parser
from inspect import getargspec
from kamaki.cli.argument import _arguments, parse_known_args, init_parser,\
update_arguments
from kamaki.cli.history import History
from kamaki.cli.utils import print_dict
from kamaki.cli.utils import print_dict, print_list, red, magenta, yellow
from kamaki.cli.errors import CLIError
_help = False
_debug = False
_verbose = False
_colors = False
def command(*args, **kwargs):
"""Dummy command decorator - replace it with UI-specific decorator"""
def wrap(cls):
return cls
return wrap
def _construct_command_syntax(cls):
spec = getargspec(cls.main.im_func)
args = spec.args[1:]
n = len(args) - len(spec.defaults or ())
required = ' '.join('<%s>' % x\
.replace('____', '[:')\
.replace('___', ':')\
.replace('__', ']').\
replace('_', ' ') for x in args[:n])
optional = ' '.join('[%s]' % x\
.replace('____', '[:')\
.replace('___', ':')\
.replace('__', ']').\
replace('_', ' ') for x in args[n:])
cls.syntax = ' '.join(x for x in [required, optional] if x)
if spec.varargs:
cls.syntax += ' <%s ...>' % spec.varargs
def _get_cmd_tree_from_spec(spec, cmd_tree_list):
for tree in cmd_tree_list:
if tree.name == spec:
return tree
return None
_best_match = []
def _num_of_matching_terms(basic_list, attack_list):
if not attack_list:
return 1
matching_terms = 0
for i, term in enumerate(basic_list):
try:
if term != attack_list[i]:
break
except IndexError:
break
matching_terms += 1
return matching_terms
def _update_best_match(name_terms, prefix=[]):
if prefix:
pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
else:
pref_list = []
num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
global _best_match
if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
if len(_best_match) < num_of_matching_terms:
_best_match = name_terms[:num_of_matching_terms]
return True
return False
def _command_load_best_match(cmd_tree_list, prefix='', descedants_depth=1):
"""Load a class as a command
spec_cmd0_cmd1 will be command spec cmd0
@cmd_tree_list is initialized in cmd_spec file and is the structure
where commands are loaded. Var name should be _commands
@param prefix if given, load only commands prefixed with prefix,
@param descedants_depth is the depth of the tree descedants of the
prefix command. It is used ONLY if prefix and if prefix is not
a terminal command
"""
def wrap(cls):
cls_name = cls.__name__
spec = cls_name.split('_')[0]
cmd_tree = _get_cmd_tree_from_spec(spec, cmd_tree_list)
if not cmd_tree:
if _debug:
print('Warning: command %s found but not loaded' % cls_name)
return cls
name_terms = cls_name.split('_')
if not _update_best_match(name_terms, prefix):
return None
global _best_match
max_len = len(_best_match) + descedants_depth
if len(name_terms) > max_len:
partial = '_'.join(name_terms[:max_len])
if not cmd_tree.has_command(partial): # add partial path
cmd_tree.add_command(partial)
return None
cls.description, sep, cls.long_description\
= cls.__doc__.partition('\n')
_construct_command_syntax(cls)
cmd_tree.add_command(cls_name, cls.description, cls)
return cls
return wrap
cmd_spec_locations = [
'kamaki.cli.commands',
'kamaki.commands',
......@@ -63,18 +176,21 @@ def _init_session(arguments):
def get_command_group(unparsed, arguments):
groups = arguments['config'].get_groups()
for grp_candidate in unparsed:
if grp_candidate in groups:
unparsed.remove(grp_candidate)
return grp_candidate
for term in unparsed:
if term.startswith('-'):
continue
if term in groups:
unparsed.remove(term)
return term
return None
return None
def _load_spec_module(spec, arguments, module):
spec_name = arguments['config'].get(spec, 'cli')
pkg = None
if spec_name is None:
spec_name = '%s_cli' % spec
return None
pkg = None
for location in cmd_spec_locations:
location += spec_name if location == '' else '.%s' % spec_name
try:
......@@ -92,7 +208,10 @@ def _groups_help(arguments):
if pkg:
cmds = None
try:
cmds = getattr(pkg, '_commands')
cmds = [
cmd for cmd in getattr(pkg, '_commands')\
if arguments['config'].get(cmd.name, 'cli')
]
except AttributeError:
if _debug:
print('Warning: No description for %s' % spec)
......@@ -108,22 +227,102 @@ def _groups_help(arguments):
print_dict(descriptions)
def _print_subcommands_help(cmd):
printout = {}
for subcmd in cmd.get_subcommands():
printout[subcmd.path.replace('_', ' ')] = subcmd.description
if printout:
print('\nOptions:\n - - - -')
print_dict(printout)
def _update_parser_help(parser, cmd):
global _best_match
parser.prog = parser.prog.split('<')[0]
parser.prog += ' '.join(_best_match)
if cmd.is_command:
cls = cmd.get_class()
parser.prog += ' ' + cls.syntax
arguments = cls().arguments
update_arguments(parser, arguments)
else:
parser.prog += ' <...>'
if cmd.has_description:
parser.description = cmd.help
def _print_error_message(cli_err):
errmsg = '%s' % cli_err
if cli_err.importance == 1:
errmsg = magenta(errmsg)
elif cli_err.importance == 2:
errmsg = yellow(errmsg)
elif cli_err.importance > 2:
errmsg = red(errmsg)
stdout.write(errmsg)
print_list(cli_err.details)
def _exec_cmd(instance, cmd_args, help_method):
try:
return instance.main(*cmd_args)
except TypeError as err:
if err.args and err.args[0].startswith('main()'):
print(magenta('Syntax error'))
if instance.get_argument('verbose'):
print(unicode(err))
help_method()
else:
raise
except CLIError as err:
if instance.get_argument('debug'):
raise
_print_error_message(err)
return 1
def one_cmd(parser, unparsed, arguments):
group = get_command_group(unparsed, arguments)
group = get_command_group(list(unparsed), arguments)
if not group:
parser.print_help()
_groups_help(arguments)
exit(0)
global command
global _command_load
command = _command_load_best_match
def_params = list(command.func_defaults)
def_params[0] = unparsed
command.func_defaults = tuple(def_params)
global _best_match
_best_match = []
spec_module = _load_spec_module(group, arguments, '_commands')
cmd_tree = _get_cmd_tree_from_spec(group, spec_module._commands)
cmd = cmd_tree.get_command('_'.join(_best_match))
_update_parser_help(parser, cmd)
if _help or not cmd.is_command:
parser.print_help()
_print_subcommands_help(cmd)
exit(0)
def interactive_shell():
print('INTERACTIVE SHELL')
cls = cmd.get_class()
executable = cls(arguments)
parsed, unparsed = parse_known_args(parser, executable.arguments)
for term in _best_match:
unparsed.remove(term)
_exec_cmd(executable, unparsed, parser.print_help)
def main():
exe = basename(argv[0])
parser = init_parser(exe, _arguments)
parsed, unparsed = parse_known_args(parser, _arguments)
print('PARSED: %s\nUNPARSED: %s' % parsed, unparsed)
if _arguments['version'].value:
exit(0)
......@@ -136,5 +335,6 @@ def main():
one_cmd(parser, unparsed, _arguments)
elif _help:
parser.print_help()
_groups_help(_arguments)
else:
interactive_shell()
print('KAMAKI SHELL IS DOWN FOR MAINTENANCE')
......@@ -69,31 +69,42 @@ def pretty_keys(d, delim='_', recurcive=False):
return new_d
def print_dict(d, exclude=(), ident=0):
def print_dict(d, exclude=(), ident=0, rjust=True):
if not isinstance(d, dict):
raise CLIError(message='Cannot dict_print a non-dict object')
try:
margin = max(
1 + max(len(unicode(key).strip()) for key in d.keys() \
if not isinstance(key, dict) and not isinstance(key, list)),
ident)
except ValueError:
if rjust:
try:
margin = max(
1 + max(len(unicode(key).strip()) for key in d.keys() \
if not (isinstance(key, dict) or isinstance(key, list))),
ident)
except ValueError:
margin = ident
else:
margin = ident
for key, val in sorted(d.items()):
if key in exclude:
continue
print_str = '%s:' % unicode(key).strip()
print_str = '%s:\t' % unicode(key).strip()
print_str = print_str.rjust(margin) if rjust\
else '%s%s' % (' ' * margin, print_str)
if isinstance(val, dict):
print(print_str.rjust(margin) + ' {')
print_dict(val, exclude=exclude, ident=margin + 6)
print '}'.rjust(margin)
print(print_str + ' {')
print_dict(val, exclude=exclude, ident=margin + 6, rjust=rjust)
if rjust:
print '}'.rjust(margin)
else:
print '}'
elif isinstance(val, list):
print(print_str.rjust(margin) + ' [')
print_list(val, exclude=exclude, ident=margin + 6)
print ']'.rjust(margin)
print(print_str + ' [')
print_list(val, exclude=exclude, ident=margin + 6, rjust=rjust)
if rjust:
print ']'.rjust(margin)
else:
print']'
else:
print print_str.rjust(margin) + ' ' + unicode(val).strip()
print print_str + ' ' + unicode(val).strip()
def print_list(l, exclude=(), ident=0):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment