Allow clis to overide command load implementation
authorStavros Sachtouris <saxtouri@admin.grnet.gr>
Mon, 12 Nov 2012 17:19:05 +0000 (19:19 +0200)
committerStavros Sachtouris <saxtouri@admin.grnet.gr>
Mon, 12 Nov 2012 17:19:05 +0000 (19:19 +0200)
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)

kamaki/cli/argument.py
kamaki/cli/command_tree.py
kamaki/cli/commands/test_cli.py
kamaki/cli/new.py
kamaki/cli/utils.py

index 663b2de..76bf589 100644 (file)
@@ -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):
index eff2f32..36d2ddb 100644 (file)
@@ -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]
 
index ee57e43..663b528 100644 (file)
 # 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)
index 8a81041..e8fb12c 100644 (file)
 # 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')
index 6138137..a83c292 100644 (file)
@@ -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):