Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument.py @ b3dd8f4b

History | View | Annotate | Download (13.7 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
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
    _parser_modified = False
362
    _parsed = None
363
    _unparsed = None
364

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

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

    
379

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

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

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

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

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

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

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

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

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

    
430
    def parse(self):
431
        self._parsed, unparsed = self.parser.parse_known_args()
432
        for name, arg in self.arguments.items():
433
            arg.value = getattr(self._parsed, name, arg.default)
434
        self._unparsed = []
435
        for term in unparsed:
436
            self._unparsed += split_input(' \'%s\' ' % term)
437
        self._parser_modified = False
438

    
439

    
440
def parse_known_args(parser, arguments=None):
441
    """Fill in arguments from user input"""
442
    parsed, unparsed = parser.parse_known_args()
443
    for name, arg in arguments.items():
444
        arg.value = getattr(parsed, name, arg.default)
445
    newparsed = []
446
    for term in unparsed:
447
        newparsed += split_input(' \'%s\' ' % term)
448
    return parsed, newparsed
449

    
450

    
451
def update_arguments(parser, arguments):
452
    """Update arguments dict from user input
453

454
    """
455
    for name, argument in arguments.items():
456
        try:
457
            argument.update_parser(parser, name)
458
        except ArgumentError:
459
            pass