Complete one-command CLI, but still doesn't work
authorStavros Sachtouris <saxtouri@admin.grnet.gr>
Fri, 28 Sep 2012 14:58:24 +0000 (17:58 +0300)
committerStavros Sachtouris <saxtouri@admin.grnet.gr>
Fri, 28 Sep 2012 14:58:24 +0000 (17:58 +0300)
missing:
1. fix the command_specs with new argument system
2. Maybe take advantage of the arbitary length of command terms for more
expressive syntax/semantics

kamaki/cli/__init__.py
kamaki/cli/argument.py
kamaki/cli/commands/pithos_cli.py
kamaki/cli/utils.py

index a3f86e7..6416ac6 100644 (file)
@@ -69,37 +69,74 @@ _commands = CommandTree(description='A command line tool for poking clouds')
 #If empty, all commands are loaded, if not empty, only commands in this list
 #e.g. [store, lele, list, lolo] is good to load store_list but not list_store
 #First arg should always refer to a group
-candidate_command_terms = [None]
+candidate_command_terms = []
+do_no_load_commands = False
+put_subclass_signatures_in_commands = False
+
+def _put_subclass_signatures_in_commands(cls):
+    global candidate_command_terms
+
+    part_name = '_'.join(candidate_command_terms)
+    try:
+        empty, same, rest = cls.__name__.partition(part_name)
+    except ValueError:
+        return False
+    if len(empty) != 0:
+        return False
+    if len(rest) == 0:
+        _commands.add_path(cls.__name__, (cls.__doc__.partition('\n'))[0])
+    else:
+        rest_terms = rest[1:].split('_')
+        new_name = part_name+'_'+rest_terms[0]
+        desc = cls.__doc__.partition('\n')[0] if new_name == cls.__name__ else ''
+        _commands.add_path(new_name, desc)
+    return True
+
+
+def _put_class_path_in_commands(cls):
+    #Maybe I should apologise for the globals, but they are used in a smart way, so...
+    global candidate_command_terms
+    term_list = cls.__name__.split('_')
+
+    tmp_tree = _commands
+    if len(candidate_command_terms) > 0:
+        #This is the case of a one-command execution: discard if not requested
+        if term_list[0] != candidate_command_terms[0]:
+            return False
+        i = 0
+        for term in term_list:
+            #check if the term is requested by user
+            if term not in candidate_command_terms[i:]:
+                return False
+            i = 1+candidate_command_terms.index(term)
+            #now, put the term in the tree
+            if term not in tmp_tree.get_command_names():
+                tmp_tree.add_command(term)
+            tmp_tree = tmp_tree.get_command(term)
+    else:
+        #Just insert everything in the tree
+        for term in term_list:
+            if term not in tmp_tree.get_command_names():
+                tmp_tree.add_command(term)
+            tmp_tree = tmp_tree.get_command()
+    return True
 
 def command():
     """Class decorator that registers a class as a CLI command"""
 
     def decorator(cls):
         """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
-        term_list = cls.__name__.split('_')
-        global candidate_command_terms
-
-        tmp_tree = _commands
-        if len(candidate_command_terms) > 0:
-            #This is the case of a one-command execution: discard if not requested
-            if term_list[0] != candidate_command_terms[0]:
-                return cls
-            i = 0
-            for term in term_list:
-                #check if the term is requested by used
-                if term not in candidate_command_terms[i:]:
-                    return cls
-                i = 1+candidate_command_terms.index(term)
-                #now, put the term in the tree
-                if term not in tmp_tree.get_command_names():
-                    tmp_tree.add_command(term)
-                tmp_tree = tmp_tree.get_command(term)
-        else:
-            #Just insert everything in the tree
-            for term in term_list:
-                if term not in tmp_tree.get_command_names():
-                    tmp_tree.add_command(term)
-                tmp_tree = tmp_tree.get_command()
+        global do_no_load_commands
+        if do_no_load_commands:
+            return cls
+
+        global put_subclass_signatures_in_commands
+        if put_subclass_signatures_in_commands:
+            _put_subclass_signatures_in_commands(cls)
+            return cls
+
+        if not _put_class_path_in_commands(cls):
+            return cls
 
         cls.description, sep, cls.long_description = cls.__doc__.partition('\n')
 
@@ -121,7 +158,7 @@ def command():
     return decorator
 
 def _init_parser(exe):
-    parser = ArgumentParser(add_help=True)
+    parser = ArgumentParser(add_help=False)
     parser.prog='%s <cmd_group> [<cmd_subbroup> ...] <cmd>'%exe
     for name, argument in _arguments.items():
         argument.update_parser(parser, name)
@@ -185,10 +222,10 @@ def _order_in_list(list1, list2):
         order += len(list2)*i*list2.index(term)
     return order
 
-def load_command(group, unparsed):
+def load_command(group, unparsed, reload_package=False):
     global candidate_command_terms
     candidate_command_terms = [group] + unparsed
-    pkg = load_group_package(group)
+    pkg = load_group_package(group, reload_package)
 
     #From all possible parsed commands, chose one
     final_cmd = group
@@ -198,7 +235,10 @@ def load_command(group, unparsed):
         if len(next_names) == 1:
             final_cmd+='_'+next_names[0]
         else:#choose the first in user string
-            pos = unparsed.index(next_names[0])
+            try:
+                pos = unparsed.index(next_names[0])
+            except ValueError:
+                return final_cmd
             choice = 0
             for i, name in enumerate(next_names[1:]):
                 tmp_index = unparsed.index(name)
@@ -207,68 +247,89 @@ def load_command(group, unparsed):
                     choice = i+1
             final_cmd+='_'+next_names[choice]
         next_names = _commands.get_command_names(final_cmd)
-    cli = _commands.get_class(final_cmd)
-    if cli is None:
-        raise CLICmdIncompleteError(details='%s'%final_cmd)
-    return cli
-
-
-def load_group_descriptions(spec_pkg):
-    for location in cmd_spec_locations:
-        location += spec_pkg if location == '' else ('.'+spec_pkg)
-        try:
-            package = __import__(location, fromlist=['API_DESCRIPTION'])
-        except ImportError:
-            continue
-        for grp, descr in package.API_DESCRIPTION.items():
-            _commands.add_command(grp, descr)
-        return package
-    raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)
+    return final_cmd
 
-def shallow_load_groups():
+def shallow_load():
     """Load only group names and descriptions"""
+    global do_no_load_commands
+    do_no_load_commands = True#load only descriptions
     for grp in _arguments['config'].get_groups():
-        spec_pkg = _arguments['config'].value.get(grp, 'cli')
-        load_group_descriptions(spec_pkg)
+        load_group_package(grp)
+    do_no_load_commands = False
 
-def load_group_package(group):
+def load_group_package(group, reload_package=False):
     spec_pkg = _arguments['config'].value.get(group, 'cli')
     for location in cmd_spec_locations:
         location += spec_pkg if location == '' else ('.'+spec_pkg)
         try:
-            package = __import__(location)
+            package = __import__(location, fromlist=['API_DESCRIPTION'])
+            if reload_package:
+                reload(package)
         except ImportError:
             continue
+        for grp, descr in package.API_DESCRIPTION.items():
+            _commands.add_command(grp, descr)
         return package
     raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)
 
-def print_commands(prefix=[]):
-    grps = {}
-    for grp in _commands.get_command_names(prefix):
-        grps[grp] = _commands.get_description(grp)
+def print_commands(prefix=[], full_tree=False):
+    cmd = _commands.get_command(prefix)
+    grps = {' . ':cmd.description} if cmd.is_command else {}
+    for grp in cmd.get_command_names():
+        grps[grp] = cmd.get_description(grp)
+    print('\nOptions:')
     print_dict(grps, ident=12)
+    if full_tree:
+        _commands.print_tree(level=-1)
 
 def one_command():
     _debug = False
+    _help = False
     try:
         exe = basename(argv[0])
         parser = _init_parser(exe)
         parsed, unparsed = parse_known_args(parser)
-        if _arguments['debug'].value:
-            _debug = True
+        _debug = _arguments['debug'].value
+        _help = _arguments['help'].value
         if _arguments['version'].value:
             exit(0)
 
         group = get_command_group(unparsed)
         if group is None:
             parser.print_help()
-            shallow_load_groups()
-            print('\nCommand groups:')
-            print_commands()
+            shallow_load()
+            print_commands(full_tree=_arguments['verbose'].value)
+            print()
+            exit(0)
+
+        command_path = load_command(group, unparsed)
+        cli = _commands.get_class(command_path)
+        if cli is None or _help: #Not a complete command
+            parser.description = _commands.closest_description(command_path)
+            parser.prog = '%s '%exe
+            for term in command_path.split('_'):
+                parser.prog += '%s '%term
+            if cli is None:
+                parser.prog += '<...>'
+            else:
+                cli().update_parser(parser)
+            parser.print_help()
+
+            #Shuuuut, we now have to load one more level just to see what is missing
+            global put_subclass_signatures_in_commands
+            put_subclass_signatures_in_commands = True
+            load_command(group, command_path.split('_')[1:], reload_package=True)
+
+            print_commands(command_path, full_tree=_arguments['verbose'].value)
             exit(0)
 
-        cli = load_command(group, unparsed)
-        print('And this is how I get my command! YEAAAAAAH! %s'%cli)
+        #Now, load the cmd
+        cmd = cli()
+        cmd.update_parser(parser)
+        parser, unparsed = parse_known_args(parser)
+        for term in command_path.split('_'):
+            unparsed.remove(term)
+        cmd.main(unparsed)
 
     except CLIError as err:
         if _debug:
index b30e012..418351f 100644 (file)
@@ -170,7 +170,7 @@ class CmdLineConfigArgument(Argument):
                                raise CLISyntaxError(details='Missing . between section and key: -o section.key=val')
                self._config_arg.value.override(section.strip(), key.strip(), val.strip())
 
-_arguments = dict(config = _config_arg,
+_arguments = dict(config = _config_arg, help = Argument(0, 'Show help message', ('-h', '--help')),
        debug = Argument(0, 'Include debug output', ('-d', '--debug')),
        include = Argument(0, 'Include protocol headers in the output', ('-i', '--include')),
        silent = Argument(0, 'Do not output anything', ('-s', '--silent')),
index 5287220..46bc72c 100644 (file)
@@ -129,10 +129,16 @@ class _store_container_command(_store_account_command):
             self.client.container = getattr(self.args,'container')
         self.container = self.client.container
 
+@command()
+class store_list_again(_store_container_command):
+    """Test stuff"""
+    def main(self):
+        pass
+
 """
 @command()
 class store_test(_store_container_command):
-    ""Test stuff""
+    "Test stuff something""
 
     def main(self):
         super(self.__class__, self).main('pithos')
@@ -1053,4 +1059,4 @@ class store_versions(_store_container_command):
         for vitem in versions:
             t = localtime(float(vitem[1]))
             vid = bold(unicode(vitem[0]))
-            print('\t%s \t(%s)'%(vid, strftime('%d-%m-%Y %H:%M:%S', t)))
\ No newline at end of file
+            print('\t%s \t(%s)'%(vid, strftime('%d-%m-%Y %H:%M:%S', t)))
index 9247158..4f7b8e4 100644 (file)
@@ -62,6 +62,17 @@ class CommandTree(object):
             terminal_cmds.append(*xtra)
         return terminal_cmds
 
+    def add_path(self, command, description):
+        path = get_pathlist_from_prefix(command)
+        tmp = self
+        for term in path:
+            try:
+                tmp = tmp.get_command(term)
+            except CLIUnknownCommand:
+                tmp.add_command(term)
+                tmp = tmp.get_command(term)
+        tmp.description = description
+
     def add_command(self, new_command, new_descr='', new_class=None):
         cmd_list = new_command.split('_')
         cmd = self.get_command(cmd_list[:-1])
@@ -93,6 +104,26 @@ class CommandTree(object):
         cmd = self.get_command(command)
         cmd.description = new_descr
 
+    def closest_complete_command(self, command):
+        path = get_pathlist_from_prefix(command)
+        tmp = self
+        choice = self
+        for term in path:
+            tmp = tmp.get_command(term)
+            if tmp.is_command():
+                choice = tmp
+        return choice
+
+    def closest_description(self, command):
+        path = get_pathlist_from_prefix(command)
+        desc = self.description
+        tmp = self
+        for term in path:
+            tmp = tmp.get_command(term)
+            if tmp.description not in [None, '']:
+                desc = tmp.description
+        return desc
+
     def copy_command(self, prefix=''):
         cmd = self.get_command(prefix)
         from copy import deepcopy
@@ -113,6 +144,15 @@ class CommandTree(object):
             raise CLIUnknownCommand('Unknown command', details=details)
         return cmd
 
+    def print_tree(self, command=[], level = 0, tabs=0):
+        cmd = self.get_command(command)
+        command_str = '_'.join(command) if isinstance(command, list) else command
+        print('   '*tabs+command_str+': '+cmd.description)
+        if level != 0:
+            for name in cmd.get_command_names():
+                new_level = level if level < 0 else (level-1)
+                cmd.print_tree(name, new_level, tabs+1)
+
 def get_pathlist_from_prefix(prefix):
     if isinstance(prefix, list):
         return prefix