e76de2e087f3a3a3ce54bebf62157876c0a1cfbd
[kamaki] / kamaki / cli / argument.py
1 # Copyright 2012 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
34 from kamaki.cli.config import Config
35 from kamaki.cli.errors import CLISyntaxError
36 from kamaki.cli.utils import split_input
37
38 from argparse import ArgumentParser, ArgumentError
39
40 try:
41     from progress.bar import FillingCirclesBar as KamakiProgressBar
42     #  IncrementalBar
43 except ImportError:
44     # progress not installed - pls, pip install progress
45     pass
46
47
48 class Argument(object):
49     """An argument that can be parsed from command line or otherwise.
50     This is the general Argument class. It is suggested to extent this
51     class into more specific argument types.
52     """
53
54     def __init__(self, arity, help=None, parsed_name=None, default=None):
55         self.arity = int(arity)
56
57         if help is not None:
58             self.help = help
59         if parsed_name is not None:
60             self.parsed_name = parsed_name
61         if default is not None:
62             self.default = default
63
64     @property
65     def parsed_name(self):
66         """the string which will be recognised by the parser as an instance
67             of this argument
68         """
69         return getattr(self, '_parsed_name', None)
70
71     @parsed_name.setter
72     def parsed_name(self, newname):
73         self._parsed_name = getattr(self, '_parsed_name', [])
74         if isinstance(newname, list) or isinstance(newname, tuple):
75             self._parsed_name += list(newname)
76         else:
77             self._parsed_name.append(unicode(newname))
78
79     @property
80     def help(self):
81         """a user friendly help message"""
82         return getattr(self, '_help', None)
83
84     @help.setter
85     def help(self, newhelp):
86         self._help = unicode(newhelp)
87
88     @property
89     def arity(self):
90         """negative for repeating, 0 for flag, 1 or more for values"""
91         return getattr(self, '_arity', None)
92
93     @arity.setter
94     def arity(self, newarity):
95         newarity = int(newarity)
96         self._arity = newarity
97
98     @property
99     def default(self):
100         """the value of this argument when not set"""
101         if not hasattr(self, '_default'):
102             self._default = False if self.arity == 0 else None
103         return self._default
104
105     @default.setter
106     def default(self, newdefault):
107         self._default = newdefault
108
109     @property
110     def value(self):
111         """the value of the argument"""
112         return getattr(self, '_value', self.default)
113
114     @value.setter
115     def value(self, newvalue):
116         self._value = newvalue
117
118     def update_parser(self, parser, name):
119         """Update argument parser with self info"""
120         action = 'append' if self.arity < 0\
121             else 'store_true' if self.arity == 0\
122             else 'store'
123         parser.add_argument(*self.parsed_name, dest=name, action=action,
124             default=self.default, help=self.help)
125
126     def main(self):
127         """Overide this method to give functionality to your args"""
128         raise NotImplementedError
129
130
131 class ConfigArgument(Argument):
132     """Manage a kamaki configuration (file)"""
133
134     _config_file = None
135
136     @property
137     def value(self):
138         """A Config object"""
139         super(self.__class__, self).value
140         return super(self.__class__, self).value
141
142     @value.setter
143     def value(self, config_file):
144         if config_file:
145             self._value = Config(config_file)
146             self._config_file = config_file
147         elif self._config_file:
148             self._value = Config(self._config_file)
149         else:
150             self._value = Config()
151
152     def get(self, group, term):
153         """Get a configuration setting from the Config object"""
154         return self.value.get(group, term)
155
156     def get_groups(self):
157         return self.value.apis()
158
159 _config_arg = ConfigArgument(1, 'Path to configuration file', '--config')
160
161
162 class CmdLineConfigArgument(Argument):
163     """Set a run-time setting option (not persistent)"""
164
165     def __init__(self, config_arg, help='', parsed_name=None, default=None):
166         super(self.__class__, self).__init__(1, help, parsed_name, default)
167         self._config_arg = config_arg
168
169     @property
170     def value(self):
171         """A key=val option"""
172         return super(self.__class__, self).value
173
174     @value.setter
175     def value(self, options):
176         if options == self.default:
177             return
178         if not isinstance(options, list):
179             options = [unicode(options)]
180         for option in options:
181             keypath, sep, val = option.partition('=')
182             if not sep:
183                 raise CLISyntaxError('Argument Syntax Error ',
184                     details='%s is missing a "=" (usage: -o section.key=val)'\
185                         % option)
186             section, sep, key = keypath.partition('.')
187         if not sep:
188             key = section
189             section = 'global'
190         self._config_arg.value.override(
191             section.strip(),
192             key.strip(),
193             val.strip())
194
195
196 class FlagArgument(Argument):
197     """
198     :value: true if set, false otherwise
199     """
200
201     def __init__(self, help='', parsed_name=None, default=False):
202         super(FlagArgument, self).__init__(0, help, parsed_name, default)
203
204
205 class ValueArgument(Argument):
206     """
207     :value type: string
208     :value returns: given value or default
209     """
210
211     def __init__(self, help='', parsed_name=None, default=None):
212         super(ValueArgument, self).__init__(1, help, parsed_name, default)
213
214
215 class IntArgument(ValueArgument):
216
217     @property
218     def value(self):
219         """integer (type checking)"""
220         return getattr(self, '_value', self.default)
221
222     @value.setter
223     def value(self, newvalue):
224         if newvalue == self.default:
225             self._value = self.default
226             return
227         try:
228             self._value = int(newvalue)
229         except ValueError:
230             raise CLISyntaxError('IntArgument Error',
231                 details='Value %s not an int' % newvalue)
232
233
234 class VersionArgument(FlagArgument):
235     """A flag argument with that prints current version"""
236
237     @property
238     def value(self):
239         """bool"""
240         return super(self.__class__, self).value
241
242     @value.setter
243     def value(self, newvalue):
244         self._value = newvalue
245         self.main()
246
247     def main(self):
248         """Print current version"""
249         if self.value:
250             import kamaki
251             print('kamaki %s' % kamaki.__version__)
252
253
254 class KeyValueArgument(Argument):
255     """A Value Argument that can be repeated
256
257     :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
258     """
259
260     def __init__(self, help='', parsed_name=None, default=[]):
261         super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
262
263     @property
264     def value(self):
265         """
266         :input: key=value
267         :output: {'key1':'value1', 'key2':'value2', ...}
268         """
269         return super(KeyValueArgument, self).value
270
271     @value.setter
272     def value(self, keyvalue_pairs):
273         self._value = {}
274         for pair in keyvalue_pairs:
275             key, sep, val = pair.partition('=')
276             if not sep:
277                 raise CLISyntaxError('Argument syntax error ',
278                     details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
279             self._value[key.strip()] = val.strip()
280
281
282 class ProgressBarArgument(FlagArgument):
283     """Manage a progress bar"""
284
285     def __init__(self, help='', parsed_name='', default=True):
286         self.suffix = '%(percent)d%%'
287         super(ProgressBarArgument, self).__init__(help, parsed_name, default)
288         try:
289             KamakiProgressBar
290         except NameError:
291             print('Warning: no progress bar functionality')
292
293     def clone(self):
294         """Get a modifiable copy of this bar"""
295         newarg = ProgressBarArgument(
296             self.help,
297             self.parsed_name,
298             self.default)
299         newarg._value = self._value
300         return newarg
301
302     def get_generator(self, message, message_len=25):
303         """Get a generator to handle progress of the bar (gen.next())"""
304         if self.value:
305             return None
306         try:
307             self.bar = KamakiProgressBar()
308         except NameError:
309             self.value = None
310             return self.value
311         self.bar.message = message.ljust(message_len)
312         self.bar.suffix = '%(percent)d%% - %(eta)ds'
313         self.bar.start()
314
315         def progress_gen(n):
316             for i in self.bar.iter(range(int(n))):
317                 yield
318             yield
319         return progress_gen
320
321     def finish(self):
322         """Stop progress bar, return terminal cursor to user"""
323         if self.value:
324             return
325         mybar = getattr(self, 'bar', None)
326         if mybar:
327             mybar.finish()
328
329
330 _arguments = dict(config=_config_arg,
331     help=Argument(0, 'Show help message', ('-h', '--help')),
332     debug=FlagArgument('Include debug output', ('-d', '--debug')),
333     include=FlagArgument('Include protocol headers in the output',
334         ('-i', '--include')),
335     silent=FlagArgument('Do not output anything', ('-s', '--silent')),
336     verbose=FlagArgument('More info at response', ('-v', '--verbose')),
337     version=VersionArgument('Print current version', ('-V', '--version')),
338     options=CmdLineConfigArgument(_config_arg,
339         'Override a config value',
340         ('-o', '--options'))
341 )
342 """Initial command line interface arguments"""
343
344
345 """
346 Mechanism:
347     init_parser
348     parse_known_args
349     manage top-level user arguments input
350     find user-requested command
351     add command-specific arguments to dict
352     update_arguments
353 """
354
355
356 class ArgumentParseManager():
357     """Manage (initialize and update) an ArgumentParser object"""
358
359     parser = ArgumentParser(add_help=False)
360     arguments = None
361
362     def __init__(self, exe, arguments=None):
363         """
364         :param exe: (str) the basic command (e.g. 'kamaki')
365
366         :param arguments: (dict) if given, overrides the global _argument as
367             the parsers arguments specification
368         """
369         if arguments:
370             self.arguments = arguments
371         else:
372             global _arguments
373             self.arguments = _arguments
374
375         self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
376         self.update_parser()
377
378     @property
379     def syntax(self):
380         return self.parser.prog
381
382     @syntax.setter
383     def syntax(self, new_syntax):
384         self.parser.prog = new_syntax
385
386     def update_parser(self, arguments=None):
387         """Load argument specifications to parser
388
389         :param arguments: if not given, update self.arguments instead
390         """
391         if not arguments:
392             arguments = self.arguments
393
394         for name, arg in arguments.items():
395             try:
396                 arg.update_parser(self.parser, name)
397             except ArgumentError:
398                 pass
399
400
401 """
402 def init_parser(exe, arguments):
403     ""Create and initialize an ArgumentParser object""
404     parser = ArgumentParser(add_help=False)
405     parser.prog = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
406     update_arguments(parser, arguments)
407     return parser
408 """
409
410
411 def parse_known_args(parser, arguments=None):
412     """Fill in arguments from user input"""
413     parsed, unparsed = parser.parse_known_args()
414     for name, arg in arguments.items():
415         arg.value = getattr(parsed, name, arg.default)
416     newparsed = []
417     for term in unparsed:
418         newparsed += split_input(' \'%s\' ' % term)
419     return parsed, newparsed
420
421
422 def update_arguments(parser, arguments):
423     """Update arguments dict from user input
424
425     """
426     for name, argument in arguments.items():
427         try:
428             argument.update_parser(parser, name)
429         except ArgumentError:
430             pass