Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument.py @ b696ed2c

History | View | Annotate | Download (13.9 kB)

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, raiseCLIError
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
                raiseCLIError(CLISyntaxError('Argument Syntax Error '),
184
                    details=['%s is missing a "="',
185
                    ' (usage: -o section.key=val)' % option]
186
                )
187
            section, sep, key = keypath.partition('.')
188
        if not sep:
189
            key = section
190
            section = 'global'
191
        self._config_arg.value.override(
192
            section.strip(),
193
            key.strip(),
194
            val.strip())
195

    
196

    
197
class FlagArgument(Argument):
198
    """
199
    :value: true if set, false otherwise
200
    """
201

    
202
    def __init__(self, help='', parsed_name=None, default=False):
203
        super(FlagArgument, self).__init__(0, help, parsed_name, default)
204

    
205

    
206
class ValueArgument(Argument):
207
    """
208
    :value type: string
209
    :value returns: given value or default
210
    """
211

    
212
    def __init__(self, help='', parsed_name=None, default=None):
213
        super(ValueArgument, self).__init__(1, help, parsed_name, default)
214

    
215

    
216
class IntArgument(ValueArgument):
217

    
218
    @property
219
    def value(self):
220
        """integer (type checking)"""
221
        return getattr(self, '_value', self.default)
222

    
223
    @value.setter
224
    def value(self, newvalue):
225
        if newvalue == self.default:
226
            self._value = self.default
227
            return
228
        try:
229
            self._value = int(newvalue)
230
        except ValueError:
231
            raiseCLIError(CLISyntaxError('IntArgument Error'),
232
                details='Value %s not an int' % newvalue)
233

    
234

    
235
class VersionArgument(FlagArgument):
236
    """A flag argument with that prints current version"""
237

    
238
    @property
239
    def value(self):
240
        """bool"""
241
        return super(self.__class__, self).value
242

    
243
    @value.setter
244
    def value(self, newvalue):
245
        self._value = newvalue
246
        self.main()
247

    
248
    def main(self):
249
        """Print current version"""
250
        if self.value:
251
            import kamaki
252
            print('kamaki %s' % kamaki.__version__)
253

    
254

    
255
class KeyValueArgument(Argument):
256
    """A Value Argument that can be repeated
257

258
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
259
    """
260

    
261
    def __init__(self, help='', parsed_name=None, default=[]):
262
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
263

    
264
    @property
265
    def value(self):
266
        """
267
        :input: key=value
268
        :output: {'key1':'value1', 'key2':'value2', ...}
269
        """
270
        return super(KeyValueArgument, self).value
271

    
272
    @value.setter
273
    def value(self, keyvalue_pairs):
274
        self._value = {}
275
        for pair in keyvalue_pairs:
276
            key, sep, val = pair.partition('=')
277
            if not sep:
278
                raiseCLIError(CLISyntaxError('Argument syntax error '),
279
                    details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
280
            self._value[key.strip()] = val.strip()
281

    
282

    
283
class ProgressBarArgument(FlagArgument):
284
    """Manage a progress bar"""
285

    
286
    def __init__(self, help='', parsed_name='', default=True):
287
        self.suffix = '%(percent)d%%'
288
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
289
        try:
290
            KamakiProgressBar
291
        except NameError:
292
            print('Warning: no progress bar functionality')
293

    
294
    def clone(self):
295
        """Get a modifiable copy of this bar"""
296
        newarg = ProgressBarArgument(
297
            self.help,
298
            self.parsed_name,
299
            self.default)
300
        newarg._value = self._value
301
        return newarg
302

    
303
    def get_generator(self, message, message_len=25):
304
        """Get a generator to handle progress of the bar (gen.next())"""
305
        if self.value:
306
            return None
307
        try:
308
            self.bar = KamakiProgressBar()
309
        except NameError:
310
            self.value = None
311
            return self.value
312
        self.bar.message = message.ljust(message_len)
313
        self.bar.suffix = '%(percent)d%% - %(eta)ds'
314
        self.bar.start()
315

    
316
        def progress_gen(n):
317
            for i in self.bar.iter(range(int(n))):
318
                yield
319
            yield
320
        return progress_gen
321

    
322
    def finish(self):
323
        """Stop progress bar, return terminal cursor to user"""
324
        if self.value:
325
            return
326
        mybar = getattr(self, 'bar', None)
327
        if mybar:
328
            mybar.finish()
329

    
330

    
331
_arguments = dict(config=_config_arg,
332
    help=Argument(0, 'Show help message', ('-h', '--help')),
333
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
334
    include=FlagArgument('Include protocol headers in the output',
335
        ('-i', '--include')),
336
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
337
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
338
    version=VersionArgument('Print current version', ('-V', '--version')),
339
    options=CmdLineConfigArgument(_config_arg,
340
        'Override a config value',
341
        ('-o', '--options'))
342
)
343
"""Initial command line interface arguments"""
344

    
345

    
346
"""
347
Mechanism:
348
    init_parser
349
    parse_known_args
350
    manage top-level user arguments input
351
    find user-requested command
352
    add command-specific arguments to dict
353
    update_arguments
354
"""
355

    
356

    
357
class ArgumentParseManager(object):
358
    """Manage (initialize and update) an ArgumentParser object"""
359

    
360
    parser = ArgumentParser(add_help=False)
361
    _arguments = {}
362
    _parser_modified = False
363
    _parsed = None
364
    _unparsed = None
365

    
366
    def __init__(self, exe, arguments=None):
367
        """
368
        :param exe: (str) the basic command (e.g. 'kamaki')
369

370
        :param arguments: (dict) if given, overrides the global _argument as
371
            the parsers arguments specification
372
        """
373
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
374
        if arguments:
375
            self.arguments = arguments
376
        else:
377
            global _arguments
378
            self.arguments = _arguments
379
        self.parse()
380

    
381
    @property
382
    def syntax(self):
383
        """The command syntax (useful for help messages, descriptions, etc)"""
384
        return self.parser.prog
385

    
386
    @syntax.setter
387
    def syntax(self, new_syntax):
388
        self.parser.prog = new_syntax
389

    
390
    @property
391
    def arguments(self):
392
        """(dict) arguments the parser should be aware of"""
393
        return self._arguments
394

    
395
    @arguments.setter
396
    def arguments(self, new_arguments):
397
        if new_arguments:
398
            assert isinstance(new_arguments, dict)
399
        self._arguments = new_arguments
400
        self.update_parser()
401

    
402
    @property
403
    def parsed(self):
404
        """(Namespace) parser-matched terms"""
405
        if self._parser_modified:
406
            self.parse()
407
        return self._parsed
408

    
409
    @property
410
    def unparsed(self):
411
        """(list) parser-unmatched terms"""
412
        if self._parser_modified:
413
            self.parse()
414
        return self._unparsed
415

    
416
    def update_parser(self, arguments=None):
417
        """Load argument specifications to parser
418

419
        :param arguments: if not given, update self.arguments instead
420
        """
421
        if not arguments:
422
            arguments = self._arguments
423

    
424
        for name, arg in arguments.items():
425
            try:
426
                arg.update_parser(self.parser, name)
427
                self._parser_modified = True
428
            except ArgumentError:
429
                pass
430

    
431
    def update_arguments(self, new_arguments):
432
        """Add to / update existing arguments
433

434
        :param new_arguments: (dict)
435
        """
436
        if new_arguments:
437
            assert isinstance(new_arguments, dict)
438
            self._arguments.update(new_arguments)
439
            self.update_parser()
440

    
441
    def parse(self, new_args=None):
442
        """Do parse user input"""
443
        if new_args:
444
            self._parsed, unparsed = self.parser.parse_known_args(new_args)
445
        else:
446
            self._parsed, unparsed = self.parser.parse_known_args()
447
        for name, arg in self.arguments.items():
448
            arg.value = getattr(self._parsed, name, arg.default)
449
        self._unparsed = []
450
        for term in unparsed:
451
            self._unparsed += split_input(' \'%s\' ' % term)
452
        self._parser_modified = False
453

    
454

    
455
def update_arguments(parser, arguments):
456
    """Update arguments dict from user input
457

458
    """
459
    for name, argument in arguments.items():
460
        try:
461
            argument.update_parser(parser, name)
462
        except ArgumentError:
463
            pass