Test pretty error details with container-not-found
[kamaki] / kamaki / cli / __init__.py
1 # Copyright 2012-2013 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.command
33
34 import logging
35 from sys import argv, exit, stdout
36 from os.path import basename
37 from inspect import getargspec
38
39 from kamaki.cli.argument import ArgumentParseManager
40 from kamaki.cli.history import History
41 from kamaki.cli.utils import print_dict, print_list, red, magenta, yellow
42 from kamaki.cli.errors import CLIError
43
44 _help = False
45 _debug = False
46 _verbose = False
47 _colors = False
48 kloger = None
49
50 #  command auxiliary methods
51
52 _best_match = []
53
54
55 def _construct_command_syntax(cls):
56         spec = getargspec(cls.main.im_func)
57         args = spec.args[1:]
58         n = len(args) - len(spec.defaults or ())
59         required = ' '.join('<%s>' % x\
60             .replace('____', '[:')\
61             .replace('___', ':')\
62             .replace('__', ']').\
63             replace('_', ' ') for x in args[:n])
64         optional = ' '.join('[%s]' % x\
65             .replace('____', '[:')\
66             .replace('___', ':')\
67             .replace('__', ']').\
68             replace('_', ' ') for x in args[n:])
69         cls.syntax = ' '.join(x for x in [required, optional] if x)
70         if spec.varargs:
71             cls.syntax += ' <%s ...>' % spec.varargs
72
73
74 def _num_of_matching_terms(basic_list, attack_list):
75     if not attack_list:
76         return len(basic_list)
77
78     matching_terms = 0
79     for i, term in enumerate(basic_list):
80         try:
81             if term != attack_list[i]:
82                 break
83         except IndexError:
84             break
85         matching_terms += 1
86     return matching_terms
87
88
89 def _update_best_match(name_terms, prefix=[]):
90     if prefix:
91         pref_list = prefix if isinstance(prefix, list) else prefix.split('_')
92     else:
93         pref_list = []
94
95     num_of_matching_terms = _num_of_matching_terms(name_terms, pref_list)
96     global _best_match
97     if not prefix:
98         _best_match = []
99
100     if num_of_matching_terms and len(_best_match) <= num_of_matching_terms:
101         if len(_best_match) < num_of_matching_terms:
102             _best_match = name_terms[:num_of_matching_terms]
103         return True
104     return False
105
106
107 def command(cmd_tree, prefix='', descedants_depth=1):
108     """Load a class as a command
109         e.g. spec_cmd0_cmd1 will be command spec cmd0
110
111         :param cmd_tree: is initialized in cmd_spec file and is the structure
112             where commands are loaded. Var name should be _commands
113         :param prefix: if given, load only commands prefixed with prefix,
114         :param descedants_depth: is the depth of the tree descedants of the
115             prefix command. It is used ONLY if prefix and if prefix is not
116             a terminal command
117
118         :returns: the specified class object
119     """
120
121     def wrap(cls):
122         global kloger
123         cls_name = cls.__name__
124
125         if not cmd_tree:
126             if _debug:
127                 kloger.warning('command %s found but not loaded' % cls_name)
128             return cls
129
130         name_terms = cls_name.split('_')
131         if not _update_best_match(name_terms, prefix):
132             if _debug:
133                 kloger.warning('%s failed to update_best_match' % cls_name)
134             return None
135
136         global _best_match
137         max_len = len(_best_match) + descedants_depth
138         if len(name_terms) > max_len:
139             partial = '_'.join(name_terms[:max_len])
140             if not cmd_tree.has_command(partial):  # add partial path
141                 cmd_tree.add_command(partial)
142             if _debug:
143                 kloger.warning('%s failed max_len test' % cls_name)
144             return None
145
146         cls.description, sep, cls.long_description\
147         = cls.__doc__.partition('\n')
148         _construct_command_syntax(cls)
149
150         cmd_tree.add_command(cls_name, cls.description, cls)
151         return cls
152     return wrap
153
154
155 cmd_spec_locations = [
156     'kamaki.cli.commands',
157     'kamaki.commands',
158     'kamaki.cli',
159     'kamaki',
160     '']
161
162
163 #  Generic init auxiliary functions
164
165
166 def _setup_logging(silent=False, debug=False, verbose=False, include=False):
167     """handle logging for clients package"""
168
169     def add_handler(name, level, prefix=''):
170         h = logging.StreamHandler()
171         fmt = logging.Formatter(prefix + '%(message)s')
172         h.setFormatter(fmt)
173         logger = logging.getLogger(name)
174         logger.addHandler(h)
175         logger.setLevel(level)
176
177     if silent:
178         add_handler('', logging.CRITICAL)
179         return
180
181     if debug:
182         add_handler('requests', logging.INFO, prefix='* ')
183         add_handler('clients.send', logging.DEBUG, prefix='> ')
184         add_handler('clients.recv', logging.DEBUG, prefix='< ')
185         add_handler('kamaki', logging.DEBUG, prefix='(debug): ')
186     elif verbose:
187         add_handler('requests', logging.INFO, prefix='* ')
188         add_handler('clients.send', logging.INFO, prefix='> ')
189         add_handler('clients.recv', logging.INFO, prefix='< ')
190         add_handler('kamaki', logging.INFO, prefix='(i): ')
191     elif include:
192         add_handler('clients.recv', logging.INFO)
193     add_handler('kamaki', logging.WARNING, prefix='(warning): ')
194     global kloger
195     kloger = logging.getLogger('kamaki')
196
197
198 def _init_session(arguments):
199     global _help
200     _help = arguments['help'].value
201     global _debug
202     _debug = arguments['debug'].value
203     global _verbose
204     _verbose = arguments['verbose'].value
205     global _colors
206     _colors = arguments['config'].get('global', 'colors')
207     if not (stdout.isatty() and _colors == 'on'):
208         from kamaki.cli.utils import remove_colors
209         remove_colors()
210     _silent = arguments['silent'].value
211     _include = arguments['include'].value
212     _setup_logging(_silent, _debug, _verbose, _include)
213
214
215 def _load_spec_module(spec, arguments, module):
216     spec_name = arguments['config'].get(spec, 'cli')
217     if spec_name is None:
218         return None
219     pkg = None
220     for location in cmd_spec_locations:
221         location += spec_name if location == '' else '.%s' % spec_name
222         try:
223             pkg = __import__(location, fromlist=[module])
224             return pkg
225         except ImportError:
226             continue
227     return pkg
228
229
230 def _groups_help(arguments):
231     global _debug
232     global kloger
233     descriptions = {}
234     for spec in arguments['config'].get_groups():
235         pkg = _load_spec_module(spec, arguments, '_commands')
236         if pkg:
237             cmds = None
238             try:
239                 cmds = [
240                     cmd for cmd in getattr(pkg, '_commands')\
241                     if arguments['config'].get(cmd.name, 'cli')
242                 ]
243             except AttributeError:
244                 if _debug:
245                     kloger.warning('No description for %s' % spec)
246             try:
247                 for cmd in cmds:
248                     descriptions[cmd.name] = cmd.description
249             except TypeError:
250                 if _debug:
251                     kloger.warning('no cmd specs in module %s' % spec)
252         elif _debug:
253             kloger.warning('Loading of %s cmd spec failed' % spec)
254     print('\nOptions:\n - - - -')
255     print_dict(descriptions)
256
257
258 def _load_all_commands(cmd_tree, arguments):
259     _config = arguments['config']
260     for spec in [spec for spec in _config.get_groups()\
261             if _config.get(spec, 'cli')]:
262         try:
263             spec_module = _load_spec_module(spec, arguments, '_commands')
264             spec_commands = getattr(spec_module, '_commands')
265         except AttributeError:
266             if _debug:
267                 global kloger
268                 kloger.warning('No valid description for %s' % spec)
269             continue
270         for spec_tree in spec_commands:
271             if spec_tree.name == spec:
272                 cmd_tree.add_tree(spec_tree)
273                 break
274
275
276 #  Methods to be used by CLI implementations
277
278
279 def print_subcommands_help(cmd):
280     printout = {}
281     for subcmd in cmd.get_subcommands():
282         spec, sep, print_path = subcmd.path.partition('_')
283         printout[print_path.replace('_', ' ')] = subcmd.description
284     if printout:
285         print('\nOptions:\n - - - -')
286         print_dict(printout)
287
288
289 def update_parser_help(parser, cmd):
290     global _best_match
291     parser.syntax = parser.syntax.split('<')[0]
292     parser.syntax += ' '.join(_best_match)
293
294     description = ''
295     if cmd.is_command:
296         cls = cmd.get_class()
297         parser.syntax += ' ' + cls.syntax
298         parser.update_arguments(cls().arguments)
299         description = getattr(cls, 'long_description', '')
300         description = description.strip()
301     else:
302         parser.syntax += ' <...>'
303     if cmd.has_description:
304         parser.parser.description = cmd.help\
305         + ((' . . . %s' % description) if description else '')
306     else:
307         parser.parser.description = description
308
309
310 def print_error_message(cli_err):
311     errmsg = '%s' % cli_err
312     if cli_err.importance == 1:
313         errmsg = magenta(errmsg)
314     elif cli_err.importance == 2:
315         errmsg = yellow(errmsg)
316     elif cli_err.importance > 2:
317         errmsg = red(errmsg)
318     stdout.write(errmsg)
319     for errmsg in cli_err.details:
320         print('| %s' % errmsg)
321
322
323 def exec_cmd(instance, cmd_args, help_method):
324     try:
325         return instance.main(*cmd_args)
326     except TypeError as err:
327         if err.args and err.args[0].startswith('main()'):
328             print(magenta('Syntax error'))
329             if _debug:
330                 raise err
331             if _verbose:
332                 print(unicode(err))
333             help_method()
334         else:
335             raise
336     return 1
337
338
339 def get_command_group(unparsed, arguments):
340     groups = arguments['config'].get_groups()
341     for term in unparsed:
342         if term.startswith('-'):
343             continue
344         if term in groups:
345             unparsed.remove(term)
346             return term
347         return None
348     return None
349
350
351 def set_command_params(parameters):
352     """Add a parameters list to a command
353
354     :param paramters: (list of str) a list of parameters
355     """
356     global command
357     def_params = list(command.func_defaults)
358     def_params[0] = parameters
359     command.func_defaults = tuple(def_params)
360
361
362 #  CLI Choice:
363
364 def run_one_cmd(exe_string, parser):
365     global _history
366     _history = History(
367         parser.arguments['config'].get('history', 'file'))
368     _history.add(' '.join([exe_string] + argv[1:]))
369     from kamaki.cli import one_command
370     one_command.run(parser, _help)
371
372
373 def run_shell(exe_string, parser):
374     from command_shell import _init_shell
375     shell = _init_shell(exe_string, parser)
376     _load_all_commands(shell.cmd_tree, parser.arguments)
377     shell.run(parser)
378
379
380 def main():
381     try:
382         exe = basename(argv[0])
383         parser = ArgumentParseManager(exe)
384
385         if parser.arguments['version'].value:
386             exit(0)
387
388         _init_session(parser.arguments)
389
390         if parser.unparsed:
391             run_one_cmd(exe, parser)
392         elif _help:
393             parser.parser.print_help()
394             _groups_help(parser.arguments)
395         else:
396             run_shell(exe, parser)
397     except CLIError as err:
398         print_error_message(err)
399         if _debug:
400             raise err
401         exit(1)
402     except Exception as er:
403         print('Unknown Error: %s' % er)
404         if _debug:
405             raise
406         exit(1)