Refactor command-tree. Better code, still buggy
authorStavros Sachtouris <saxtouri@admin.grnet.gr>
Fri, 5 Oct 2012 15:03:47 +0000 (18:03 +0300)
committerStavros Sachtouris <saxtouri@admin.grnet.gr>
Fri, 5 Oct 2012 15:03:47 +0000 (18:03 +0300)
kamaki/cli/__init__.py
kamaki/cli/command.py
kamaki/cli/utils.py

index 06f5520..469bc25 100644 (file)
@@ -55,7 +55,8 @@ except ImportError:
 #from kamaki import clients
 from .errors import CLIError, CLISyntaxError, CLICmdIncompleteError, CLICmdSpecError
 from .config import Config #TO BE REMOVED
-from .utils import bold, magenta, red, yellow, CommandTree, print_list, print_dict
+from .utils import bold, magenta, red, yellow, print_list, print_dict
+from .command import CommandTree
 from argument import _arguments, parse_known_args
 
 cmd_spec_locations = [
@@ -64,78 +65,49 @@ cmd_spec_locations = [
     'kamaki.cli',
     'kamaki',
     '']
-_commands = CommandTree(description='A command line tool for poking clouds')
+_commands = CommandTree(name='kamaki', 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 = []
-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:
+allow_no_commands = False
+allow_all_commands = False
+allow_subclass_signatures = False
+
+def _allow_class_in_cmd_tree(cls):
+    global allow_all_commands
+    if allow_all_commands:
+        return True
+    global allow_no_commands 
+    if allow_no_commands:
         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]:
+    global candidate_command_terms
+    index = 0
+    for term in candidate_command_terms:
+        try:
+            index += 1 if term_list[index] == term else 0
+        except IndexError:
             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
+    if allow_subclass_signatures:
+        if index == len(candidate_command_terms) and len(term_list) > index:
+            try: #is subterm already in _commands?
+                _commands.get_command('_'.join(term_list[:index+1]))
+            except KeyError: #No, so it must be placed there
+                return True
+        return False
+
+    return True if index == len(term_list) else False
 
 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"""
-        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):
+        if not _allow_class_in_cmd_tree(cls):
             return cls
 
         cls.description, sep, cls.long_description = cls.__doc__.partition('\n')
@@ -184,36 +156,6 @@ def _print_error_message(cli_err):
     else:
         print()
 
-def _expand_cmd(cmd_prefix, unparsed):
-    if len(unparsed) == 0:
-        return None
-    prefix = (cmd_prefix+'_') if len(cmd_prefix) > 0 else ''
-    for term in _commands.list(cmd_prefix):
-        try:
-            unparsed.remove(term)
-        except ValueError:
-            continue
-        return prefix+term
-    return None
-
-def _retrieve_cmd(unparsed):
-    cmd_str = None
-    cur_cmd = _expand_cmd('', unparsed)
-    while cur_cmd is not None:
-        cmd_str = cur_cmd
-        cur_cmd = _expand_cmd(cur_cmd, unparsed)
-    if cmd_str is None:
-        print(bold('Command groups:'))
-        print_list(_commands.get_groups(), ident=14)
-        print
-        return None
-    try:
-        return _commands.get_class(cmd_str)
-    except CLICmdIncompleteError:
-        print(bold('%s:'%cmd_str))
-        print_list(_commands.list(cmd_str))
-    return None
-
 def get_command_group(unparsed):
     groups = _arguments['config'].get_groups()
     for grp_candidate in unparsed:
@@ -222,46 +164,27 @@ def get_command_group(unparsed):
             return grp_candidate
     return None
 
-def _order_in_list(list1, list2):
-    order = 0
-    for i,term in enumerate(list1):
-        order += len(list2)*i*list2.index(term)
-    return order
-
 def load_command(group, unparsed, reload_package=False):
     global candidate_command_terms
     candidate_command_terms = [group] + unparsed
     pkg = load_group_package(group, reload_package)
 
-    #From all possible parsed commands, chose one
-    final_cmd = group
-    next_names = [None]
-    next_names = _commands.get_command_names(final_cmd)
-    while len(next_names) > 0:
-        if len(next_names) == 1:
-            final_cmd+='_'+next_names[0]
-        else:#choose the first in user string
-            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)
-                if tmp_index < pos:
-                    pos = tmp_index
-                    choice = i+1
-            final_cmd+='_'+next_names[choice]
-        next_names = _commands.get_command_names(final_cmd)
+    #From all possible parsed commands, chose the first match in user string
+    final_cmd = _commands.get_command(group)
+    for term in unparsed:
+        cmd = final_cmd.get_subcmd(term)
+        if cmd is not None:
+            final_cmd = cmd
+            unparsed.remove(cmd.name)
     return final_cmd
 
 def shallow_load():
     """Load only group names and descriptions"""
-    global do_no_load_commands
-    do_no_load_commands = True#load only descriptions
+    global allow_no_commands 
+    allow_no_commands = True#load only descriptions
     for grp in _arguments['config'].get_groups():
         load_group_package(grp)
-    do_no_load_commands = False
+    allow_no_commands = False
 
 def load_group_package(group, reload_package=False):
     spec_pkg = _arguments['config'].value.get(group, 'cli')
@@ -269,34 +192,41 @@ def load_group_package(group, reload_package=False):
         location += spec_pkg if location == '' else ('.'+spec_pkg)
         try:
             package = __import__(location, fromlist=['API_DESCRIPTION'])
-            if reload_package:
-                reload(package)
         except ImportError:
             continue
+        if reload_package:
+            reload(package)
         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=[], 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 print_commands(prefix=None, full_depth=False):
+    cmd_list = _commands.get_groups() if prefix is None else _commands.get_subcommands(prefix)
+    cmds = {}
+    for subcmd in cmd_list:
+        if subcmd.sublen() > 0:
+            sublen_str = '( %s more terms ... )'%subcmd.sublen()
+            cmds[subcmd.name] = [subcmd.help, sublen_str] if subcmd.has_description else subcmd_str
+        else:
+            cmds[subcmd.name] = subcmd.help
+    if len(cmds) > 0:
+        print('\nOptions:')
+        print_dict(cmds, ident=12)
+    if full_depth:
+        _commands.pretty_print()
 
 def one_command():
     _debug = False
     _help = False
+    _verbose = False
     try:
         exe = basename(argv[0])
         parser = _init_parser(exe)
         parsed, unparsed = parse_known_args(parser)
         _debug = _arguments['debug'].value
         _help = _arguments['help'].value
+        _verbose = _arguments['verbose'].value
         if _arguments['version'].value:
             exit(0)
 
@@ -304,39 +234,42 @@ def one_command():
         if group is None:
             parser.print_help()
             shallow_load()
-            print_commands(full_tree=_arguments['verbose'].value)
-            print()
+            print_commands(full_depth=_verbose)
             exit(0)
 
-        command_path = load_command(group, unparsed)
-        cli = _commands.get_class(command_path)
-        if cli is None or _help: #Not a complete command or help
-            parser.description = _commands.closest_description(command_path)
-            parser.prog = '%s %s '%(exe, command_path.replace('_', ' '))
-            if cli is None:
-                parser.prog += '<...>'
+        cmd = load_command(group, unparsed)
+        if _help or not cmd.is_command:
+            if cmd.has_description:
+                parser.description = cmd.help 
             else:
+                try:
+                    parser.description = _commands.get_closest_ancestor_command(cmd.path).help
+                except KeyError:
+                    parser.description = ' '
+            parser.prog = '%s %s '%(exe, cmd.path.replace('_', ' '))
+            if cmd.is_command:
+                cli = cmd.get_class()
                 parser.prog += cli.syntax
                 _update_parser(parser, cli().arguments)
+            else:
+                parser.prog += '[...]'
             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)
+            global allow_subclass_signatures 
+            allow_subclass_signatures = True
+            load_command(group, cmd.path.split('_')[1:], reload_package=True)
 
-            print_commands(command_path, full_tree=_arguments['verbose'].value)
+            print_commands(cmd.path, full_depth=_verbose)
             exit(0)
 
-        #Now, load the cmd
-        cmd = cli(_arguments)
-        _update_parser(parser, cmd.arguments)
-        parser.prog = '%s %s %s'%(exe, command_path.replace('_', ' '), cli.syntax)
-        parsed, unparsed = parse_known_args(parser)
-        for term in command_path.split('_'):
-            unparsed.remove(term)
+        cli = cmd.get_class()
+        executable = cli(_arguments)
+        _update_parser(parser, executable.arguments)
+        parser.prog = '%s %s %s'%(exe, cmd.path.replace('_', ' '), cli.syntax)
+        parse_known_args(parser)
         try:
-            ret = cmd.main(*unparsed)
+            ret = executable.main(*unparsed)
             exit(ret)
         except TypeError as e:
             if e.args and e.args[0].startswith('main()'):
index fa70402..e3cdfb6 100644 (file)
@@ -32,6 +32,7 @@
 # or implied, of GRNET S.A.
 
 import cmd
+#from .errors import CLIUnknownCommand, CLICmdIncompleteError, CLICmdSpecError, CLIError
 
 class Command(object):
        """Store a command and the next-level commands as well - no deep tree here"""
@@ -68,8 +69,15 @@ class Command(object):
                """Check if a name is a direct child of self"""
                return self.subcommands.has_key(name)
 
+       @property 
        def is_command(self):
                return self.cmd_class is not None
+       @property 
+       def has_description(self):
+               return len(self.help.strip()) > 0
+       @property 
+       def description(self):
+               return self.help
 
        def set_class(self, cmd_class):
                self.cmd_class = cmd_class
@@ -85,7 +93,7 @@ class Command(object):
 
        def pretty_print(self, recursive=False):
                print('Path: %s (Name: %s) is_cmd: %s\n\thelp: %s'%(self.path, self.name,
-                       self.is_command(), self.help))
+                       self.is_command, self.help))
                for cmd in self.get_subcommands():
                        cmd.pretty_print(recursive)
 
@@ -97,6 +105,116 @@ def test_Command():
        tmp.add_subcmd(Command('store_list_one', 'List just one stuff'))
        cmd.pretty_print(True)
 
+class CommandTree(object):
+
+       groups = {}
+       _all_commands = {}
+       name = None
+       description = None
+
+       def __init__(self, name, description=''):
+               self.name = name
+               self.description = description
+
+       def add_command(self, command_path, description=None, cmd_class=None):
+               terms = command_path.split('_')
+               try:
+                       cmd = self.groups[terms[0]]
+               except KeyError:
+                       cmd = Command(terms[0])
+                       self.groups[terms[0]] = cmd
+                       self._all_commands[terms[0]] = cmd
+               path = terms[0]
+               for term in terms[1:]:
+                       path += '_'+term
+                       try:
+                               cmd = cmd.subcommands[term]
+                       except KeyError:
+                               new_cmd = Command(path)
+                               self._all_commands[path] = new_cmd
+                               cmd.add_subcmd(new_cmd)
+                               cmd = new_cmd
+               if cmd_class is not None:
+                       cmd.set_class(cmd_class)
+               if description is not None:
+                       cmd.help = description
+       def get_command(self, path):
+               return self._all_commands[path]
+       def get_groups(self):
+               return self.groups.values()
+       def get_group_names(self):
+               return self.groups.keys()
+
+       def set_description(self, path, description):
+               self._all_commands[path].help = description
+       def get_descitpion(self, path):
+               return self._all_commands[path].help
+       def set_class(self, path, cmd_class):
+               self._all_commands[path].set_class(cmd_class)
+       def get_class(self, path):
+               return self._all_commands[path].get_class()
+
+       def get_subnames(self, path):
+               return self._all_commands[path].get_subnames()
+       def get_subcommands(self, path):
+               return self._all_commands[path].get_subcommands()
+       def get_parent(self, path):
+               if '_' not in path:
+                       return None
+               terms = path.split('_')
+               parent_path = '_'.join(terms[:-1])
+               return self._all_commands[parent_path]
+       def get_closest_ancestor_command(self, path):
+               path, sep, name = path.rpartition('_')
+               while len(path) > 0:
+                       cmd = self._all_commands[path]
+                       if cmd.is_command:
+                               return cmd
+                       path, sep, name = path.rpartition('_')
+               return None
+
+               if '_' not in path:
+                       return None
+               terms = terms[:-1]
+               while len(terms) > 0:
+                       tmp_path = '_'.join(terms)
+                       cmd = self._all_commands[tmp_path]
+                       if cmd.is_command:
+                               return cmd
+                       terms = terms[:-1]
+               raise KeyError('No ancestor commands')
+
+       def pretty_print(self, group=None):
+               if group is None:
+                       for group in self.groups:
+                               self.pretty_print(group)
+               else:
+                       self.groups[group].pretty_print(recursive=True)
+
+def test_CommandTree():
+       tree = CommandTree('kamaki', 'the kamaki tools')
+       tree.add_command('store', 'A storage thingy')
+       tree.add_command('server_list_lala', description='A testing server list', cmd_class=Shell)
+       tree.add_command('store_list_all', 'List all things', cmd_class=Command)
+       tree.add_command('store_list', 'List smthing pls', cmd_class=Shell)
+       tree.add_command('server_list', description='A server list subgrp')
+       tree.add_command('server', description='A server is a SERVER', cmd_class=CommandTree)
+       tree.set_class('server', None)
+       tree.set_description('server_list', '')
+       if tree.get_class('server_list_lala') is Shell:
+               print('server_list_lala is Shell')
+       else:
+               print('server_list_lala is not Shell')
+       tree.pretty_print()
+       print('store_list_all closest parent command is %s'%tree.get_closest_ancestor_command('store_list_all').path)
+       tree.set_class('store', tree.get_command('store_list').get_class())
+       tree.set_class('store_list', None)
+       print('store_list_all closest parent command is %s'%tree.get_closest_ancestor_command('store_list_all').path)
+       try:
+               print('nonexisting_list_command closest parent is %s'%tree.get_closest_ancestor_command('nonexisting_list_command').path)
+       except KeyError:
+               print('Aparrently nonexisting_list_command is nonexisting ')
+
 class Shell(cmd.Cmd):
        """Simple command processor example."""
 
@@ -125,4 +243,4 @@ class Shell(cmd.Cmd):
 
 #import sys
 #sh.onecmd(' '.join(sys.argv[1:]))
-test_Command()
\ No newline at end of file
+#test_CommandTree()
\ No newline at end of file
index 4f7b8e4..cd32eb1 100644 (file)
@@ -38,127 +38,7 @@ except ImportError:
         return val
     red = yellow = magenta = bold
 
-from .errors import CLIUnknownCommand, CLICmdIncompleteError, CLICmdSpecError, CLIError
 
-class CommandTree(object):
-    """A tree of command terms usefull for fast commands checking
-    """
-
-    def __init__(self, run_class=None, description='', commands={}):
-        self.run_class = run_class
-        self.description = description
-        self.commands = commands
-                
-    def get_command_names(self, prefix=[]):
-        cmd = self.get_command(prefix)
-        return cmd.commands.keys()
-
-    def get_terminal_commands(self, prefix=''):
-        cmd = self.get_command(prefix)
-        terminal_cmds = [prefix] if cmd.is_command() else []
-        prefix = '' if len(prefix) == 0 else '%s_'%prefix
-        for term, tree in cmd.commands.items():
-            xtra = self.get_terminal_commands(prefix+term)
-            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])
-        try:
-            existing = cmd.get_command(cmd_list[-1])
-            if new_class is not None:
-                existing.run_class = new_class
-            if new_descr not in (None, ''):
-                existing.description = new_descr
-        except CLIUnknownCommand:
-            cmd.commands[new_command] = CommandTree(new_class,new_descr,{})
-
-    def is_command(self, command=''):
-        if self.get_command(command).run_class is None:
-            return False
-        return True
-
-    def get_class(self, command=''):
-        cmd = self.get_command(command)
-        return cmd.run_class
-    def set_class(self, command, new_class):
-        cmd = self.get_command(command)
-        cmd.run_class = new_class
-
-    def get_description(self, command):
-        cmd = self.get_command(command)
-        return cmd.description
-    def set_description(self, command, new_descr):
-        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
-        return deepcopy(cmd)
-
-    def get_command(self, command):
-        """
-        @return a tuple of the form (cls_object, 'description text', {term1':(...), 'term2':(...)})
-        """
-        path = get_pathlist_from_prefix(command)
-        cmd = self
-        try:
-            for term in path:
-                cmd = cmd.commands[term]
-        except KeyError:
-            error_index = path.index(term)
-            details='Command term %s not in path %s'%(unicode(term), path[:error_index])
-            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
-    if len(prefix) == 0:
-        return []
-    return unicode(prefix).split('_')
 
 def pretty_keys(d, delim='_', recurcive=False):
     """Transform keys of a dict from the form