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