Complete one-command CLI, but still doesn't work
[kamaki] / kamaki / cli / utils.py
1 # Copyright 2011 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33 try:
34     from colors import magenta, red, yellow, bold
35 except ImportError:
36     #No colours? No worries, use dummy foo instead
37     def bold(val):
38         return val
39     red = yellow = magenta = bold
40
41 from .errors import CLIUnknownCommand, CLICmdIncompleteError, CLICmdSpecError, CLIError
42
43 class CommandTree(object):
44     """A tree of command terms usefull for fast commands checking
45     """
46
47     def __init__(self, run_class=None, description='', commands={}):
48         self.run_class = run_class
49         self.description = description
50         self.commands = commands
51                 
52     def get_command_names(self, prefix=[]):
53         cmd = self.get_command(prefix)
54         return cmd.commands.keys()
55
56     def get_terminal_commands(self, prefix=''):
57         cmd = self.get_command(prefix)
58         terminal_cmds = [prefix] if cmd.is_command() else []
59         prefix = '' if len(prefix) == 0 else '%s_'%prefix
60         for term, tree in cmd.commands.items():
61             xtra = self.get_terminal_commands(prefix+term)
62             terminal_cmds.append(*xtra)
63         return terminal_cmds
64
65     def add_path(self, command, description):
66         path = get_pathlist_from_prefix(command)
67         tmp = self
68         for term in path:
69             try:
70                 tmp = tmp.get_command(term)
71             except CLIUnknownCommand:
72                 tmp.add_command(term)
73                 tmp = tmp.get_command(term)
74         tmp.description = description
75
76     def add_command(self, new_command, new_descr='', new_class=None):
77         cmd_list = new_command.split('_')
78         cmd = self.get_command(cmd_list[:-1])
79         try:
80             existing = cmd.get_command(cmd_list[-1])
81             if new_class is not None:
82                 existing.run_class = new_class
83             if new_descr not in (None, ''):
84                 existing.description = new_descr
85         except CLIUnknownCommand:
86             cmd.commands[new_command] = CommandTree(new_class,new_descr,{})
87
88     def is_command(self, command=''):
89         if self.get_command(command).run_class is None:
90             return False
91         return True
92
93     def get_class(self, command=''):
94         cmd = self.get_command(command)
95         return cmd.run_class
96     def set_class(self, command, new_class):
97         cmd = self.get_command(command)
98         cmd.run_class = new_class
99
100     def get_description(self, command):
101         cmd = self.get_command(command)
102         return cmd.description
103     def set_description(self, command, new_descr):
104         cmd = self.get_command(command)
105         cmd.description = new_descr
106
107     def closest_complete_command(self, command):
108         path = get_pathlist_from_prefix(command)
109         tmp = self
110         choice = self
111         for term in path:
112             tmp = tmp.get_command(term)
113             if tmp.is_command():
114                 choice = tmp
115         return choice
116
117     def closest_description(self, command):
118         path = get_pathlist_from_prefix(command)
119         desc = self.description
120         tmp = self
121         for term in path:
122             tmp = tmp.get_command(term)
123             if tmp.description not in [None, '']:
124                 desc = tmp.description
125         return desc
126
127     def copy_command(self, prefix=''):
128         cmd = self.get_command(prefix)
129         from copy import deepcopy
130         return deepcopy(cmd)
131
132     def get_command(self, command):
133         """
134         @return a tuple of the form (cls_object, 'description text', {term1':(...), 'term2':(...)})
135         """
136         path = get_pathlist_from_prefix(command)
137         cmd = self
138         try:
139             for term in path:
140                 cmd = cmd.commands[term]
141         except KeyError:
142             error_index = path.index(term)
143             details='Command term %s not in path %s'%(unicode(term), path[:error_index])
144             raise CLIUnknownCommand('Unknown command', details=details)
145         return cmd
146
147     def print_tree(self, command=[], level = 0, tabs=0):
148         cmd = self.get_command(command)
149         command_str = '_'.join(command) if isinstance(command, list) else command
150         print('   '*tabs+command_str+': '+cmd.description)
151         if level != 0:
152             for name in cmd.get_command_names():
153                 new_level = level if level < 0 else (level-1)
154                 cmd.print_tree(name, new_level, tabs+1)
155
156 def get_pathlist_from_prefix(prefix):
157     if isinstance(prefix, list):
158         return prefix
159     if len(prefix) == 0:
160         return []
161     return unicode(prefix).split('_')
162
163 def pretty_keys(d, delim='_', recurcive=False):
164     """Transform keys of a dict from the form
165     str1_str2_..._strN to the form strN
166     where _ is the delimeter
167     """
168     new_d = {}
169     for key, val in d.items():
170         new_key = key.split(delim)[-1]
171         if recurcive and isinstance(val, dict):
172             new_val = pretty_keys(val, delim, recurcive) 
173         else:
174             new_val = val
175         new_d[new_key] = new_val
176     return new_d
177
178 def print_dict(d, exclude=(), ident= 0):
179     if not isinstance(d, dict):
180         raise CLIError(message='Cannot dict_print a non-dict object')
181     try:
182         margin = max(
183             1 + max(len(unicode(key).strip()) for key in d.keys() \
184                 if not isinstance(key, dict) and not isinstance(key, list)),
185             ident)
186     except ValueError:
187         margin = ident
188
189     for key, val in sorted(d.items()):
190         if key in exclude:
191             continue
192         print_str = '%s:' % unicode(key).strip()
193         if isinstance(val, dict):
194             print(print_str.rjust(margin)+' {')
195             print_dict(val, exclude = exclude, ident = margin + 6)
196             print '}'.rjust(margin)
197         elif isinstance(val, list):
198             print(print_str.rjust(margin)+' [')
199             print_list(val, exclude = exclude, ident = margin + 6)
200             print ']'.rjust(margin)
201         else:
202             print print_str.rjust(margin)+' '+unicode(val).strip()
203
204 def print_list(l, exclude=(), ident = 0):
205     if not isinstance(l, list):
206         raise CLIError(message='Cannot list_print a non-list object')
207     try:
208         margin = max(
209             1 + max(len(unicode(item).strip()) for item in l \
210                 if not isinstance(item, dict) and not isinstance(item, list)),
211             ident)
212     except ValueError:
213         margin = ident
214
215     for item in sorted(l):
216         if item in exclude:
217             continue
218         if isinstance(item, dict):
219             print('{'.rjust(margin))
220             print_dict(item, exclude = exclude, ident = margin + 6)
221             print '}'.rjust(margin)
222         elif isinstance(item, list):
223             print '['.rjust(margin)
224             print_list(item, exclude = exclude, ident = margin + 6)
225             print ']'.rjust(margin)
226         else:
227             print unicode(item).rjust(margin)
228
229 def print_items(items, title=('id', 'name')):
230     for item in items:
231         if isinstance(item, dict) or isinstance(item, list):
232             print ' '.join(unicode(item.pop(key)) for key in title if key in item)
233         if isinstance(item, dict):
234             print_dict(item)
235
236 def format_size(size):
237     units = ('B', 'K', 'M', 'G', 'T')
238     try:
239         size = float(size)
240     except ValueError:
241         raise CLIError(message='Cannot format %s in bytes'%size)
242     for unit in units:
243         if size < 1024:
244             break
245         size /= 1024
246     s = ('%.1f' % size)
247     if '.0' == s[-2:]:
248         s = s[:-2]
249     return s + unit
250
251 def dict2file(d, f, depth = 0):
252     for k, v in d.items():
253         f.write('%s%s: '%('\t'*depth, k))
254         if isinstance(v,dict):
255             f.write('\n')
256             dict2file(v, f, depth+1)
257         elif isinstance(v,list):
258             f.write('\n')
259             list2file(v, f, depth+1)
260         else:
261             f.write(' %s\n'%unicode(v))
262
263 def list2file(l, f, depth = 1):
264     for item in l:
265         if isinstance(item,dict):
266             dict2file(item, f, depth+1)
267         elif isinstance(item,list):
268             list2file(item, f, depth+1)
269         else:
270             f.write('%s%s\n'%('\t'*depth, unicode(item)))