Restructure interface code
[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     if cmd.is_command:
295         cls = cmd.get_class()
296         parser.syntax += ' ' + cls.syntax
297         parser.update_arguments(cls().arguments)
298         # arguments = cls().arguments
299         # update_arguments(parser, arguments)
300     else:
301         parser.syntax += ' <...>'
302     if cmd.has_description:
303         parser.parser.description = cmd.help
304
305
306 def print_error_message(cli_err):
307     errmsg = '%s' % cli_err
308     if cli_err.importance == 1:
309         errmsg = magenta(errmsg)
310     elif cli_err.importance == 2:
311         errmsg = yellow(errmsg)
312     elif cli_err.importance > 2:
313         errmsg = red(errmsg)
314     stdout.write(errmsg)
315     print_list(cli_err.details)
316
317
318 def exec_cmd(instance, cmd_args, help_method):
319     try:
320         return instance.main(*cmd_args)
321     except TypeError as err:
322         if err.args and err.args[0].startswith('main()'):
323             print(magenta('Syntax error'))
324             if _debug:
325                 raise err
326             if _verbose:
327                 print(unicode(err))
328             help_method()
329         else:
330             raise
331     return 1
332
333
334 def get_command_group(unparsed, arguments):
335     groups = arguments['config'].get_groups()
336     for term in unparsed:
337         if term.startswith('-'):
338             continue
339         if term in groups:
340             unparsed.remove(term)
341             return term
342         return None
343     return None
344
345
346 def set_command_params(parameters):
347     """Add a parameters list to a command
348
349     :param paramters: (list of str) a list of parameters
350     """
351     global command
352     def_params = list(command.func_defaults)
353     def_params[0] = parameters
354     command.func_defaults = tuple(def_params)
355
356
357 #  CLI Choice:
358
359 def run_one_cmd(exe_string, parser):
360     global _history
361     _history = History(
362         parser.arguments['config'].get('history', 'file'))
363     _history.add(' '.join([exe_string] + argv[1:]))
364     from kamaki.cli import one_command
365     one_command.run(parser, _help)
366
367
368 def run_shell(exe_string, parser):
369     from command_shell import _init_shell
370     shell = _init_shell(exe_string, parser)
371     _load_all_commands(shell.cmd_tree, parser.arguments)
372     shell.run(parser)
373
374
375 def main():
376     try:
377         exe = basename(argv[0])
378         parser = ArgumentParseManager(exe)
379
380         if parser.arguments['version'].value:
381             exit(0)
382
383         _init_session(parser.arguments)
384
385         if parser.unparsed:
386             run_one_cmd(exe, parser)
387         elif _help:
388             parser.parser.print_help()
389             _groups_help(parser.arguments)
390         else:
391             run_shell(exe, parser)
392     except CLIError as err:
393         print_error_message(err)
394         if _debug:
395             raise err
396         exit(1)
397     except Exception as er:
398         print('Unknown Error: %s' % er)
399         if _debug:
400             raise
401         exit(1)