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