Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument.py @ b6a99832

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, raiseCLIError
36
from kamaki.cli.utils import split_input
37
from logging import getLogger
38

    
39

    
40
from argparse import ArgumentParser, ArgumentError
41

    
42
try:
43
    from progress.bar import ShadyBar as KamakiProgressBar
44
except ImportError:
45
    # progress not installed - pls, pip install progress
46
    pass
47

    
48
kloger = getLogger('kamaki')
49

    
50

    
51
class Argument(object):
52
    """An argument that can be parsed from command line or otherwise.
53
    This is the general Argument class. It is suggested to extent this
54
    class into more specific argument types.
55
    """
56

    
57
    def __init__(self, arity, help=None, parsed_name=None, default=None):
58
        self.arity = int(arity)
59

    
60
        if help is not None:
61
            self.help = help
62
        if parsed_name is not None:
63
            self.parsed_name = parsed_name
64
        if default is not None:
65
            self.default = default
66

    
67
    @property
68
    def parsed_name(self):
69
        """the string which will be recognised by the parser as an instance
70
            of this argument
71
        """
72
        return getattr(self, '_parsed_name', None)
73

    
74
    @parsed_name.setter
75
    def parsed_name(self, newname):
76
        self._parsed_name = getattr(self, '_parsed_name', [])
77
        if isinstance(newname, list) or isinstance(newname, tuple):
78
            self._parsed_name += list(newname)
79
        else:
80
            self._parsed_name.append(unicode(newname))
81

    
82
    @property
83
    def help(self):
84
        """a user friendly help message"""
85
        return getattr(self, '_help', None)
86

    
87
    @help.setter
88
    def help(self, newhelp):
89
        self._help = unicode(newhelp)
90

    
91
    @property
92
    def arity(self):
93
        """negative for repeating, 0 for flag, 1 or more for values"""
94
        return getattr(self, '_arity', None)
95

    
96
    @arity.setter
97
    def arity(self, newarity):
98
        newarity = int(newarity)
99
        self._arity = newarity
100

    
101
    @property
102
    def default(self):
103
        """the value of this argument when not set"""
104
        if not hasattr(self, '_default'):
105
            self._default = False if self.arity == 0 else None
106
        return self._default
107

    
108
    @default.setter
109
    def default(self, newdefault):
110
        self._default = newdefault
111

    
112
    @property
113
    def value(self):
114
        """the value of the argument"""
115
        return getattr(self, '_value', self.default)
116

    
117
    @value.setter
118
    def value(self, newvalue):
119
        self._value = newvalue
120

    
121
    def update_parser(self, parser, name):
122
        """Update argument parser with self info"""
123
        action = 'append' if self.arity < 0\
124
            else 'store_true' if self.arity == 0\
125
            else 'store'
126
        parser.add_argument(*self.parsed_name, dest=name, action=action,
127
            default=self.default, help=self.help)
128

    
129
    def main(self):
130
        """Overide this method to give functionality to your args"""
131
        raise NotImplementedError
132

    
133

    
134
class ConfigArgument(Argument):
135
    """Manage a kamaki configuration (file)"""
136

    
137
    _config_file = None
138

    
139
    @property
140
    def value(self):
141
        """A Config object"""
142
        super(self.__class__, self).value
143
        return super(self.__class__, self).value
144

    
145
    @value.setter
146
    def value(self, config_file):
147
        if config_file:
148
            self._value = Config(config_file)
149
            self._config_file = config_file
150
        elif self._config_file:
151
            self._value = Config(self._config_file)
152
        else:
153
            self._value = Config()
154

    
155
    def get(self, group, term):
156
        """Get a configuration setting from the Config object"""
157
        return self.value.get(group, term)
158

    
159
    def get_groups(self):
160
        return self.value.apis()
161

    
162
_config_arg = ConfigArgument(1, 'Path to configuration file', '--config')
163

    
164

    
165
class CmdLineConfigArgument(Argument):
166
    """Set a run-time setting option (not persistent)"""
167

    
168
    def __init__(self, config_arg, help='', parsed_name=None, default=None):
169
        super(self.__class__, self).__init__(1, help, parsed_name, default)
170
        self._config_arg = config_arg
171

    
172
    @property
173
    def value(self):
174
        """A key=val option"""
175
        return super(self.__class__, self).value
176

    
177
    @value.setter
178
    def value(self, options):
179
        if options == self.default:
180
            return
181
        if not isinstance(options, list):
182
            options = [unicode(options)]
183
        for option in options:
184
            keypath, sep, val = option.partition('=')
185
            if not sep:
186
                raiseCLIError(CLISyntaxError('Argument Syntax Error '),
187
                    details=['%s is missing a "="',
188
                    ' (usage: -o section.key=val)' % option]
189
                )
190
            section, sep, key = keypath.partition('.')
191
        if not sep:
192
            key = section
193
            section = 'global'
194
        self._config_arg.value.override(
195
            section.strip(),
196
            key.strip(),
197
            val.strip())
198

    
199

    
200
class FlagArgument(Argument):
201
    """
202
    :value: true if set, false otherwise
203
    """
204

    
205
    def __init__(self, help='', parsed_name=None, default=False):
206
        super(FlagArgument, self).__init__(0, help, parsed_name, default)
207

    
208

    
209
class ValueArgument(Argument):
210
    """
211
    :value type: string
212
    :value returns: given value or default
213
    """
214

    
215
    def __init__(self, help='', parsed_name=None, default=None):
216
        super(ValueArgument, self).__init__(1, help, parsed_name, default)
217

    
218

    
219
class IntArgument(ValueArgument):
220

    
221
    @property
222
    def value(self):
223
        """integer (type checking)"""
224
        return getattr(self, '_value', self.default)
225

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

    
237

    
238
class VersionArgument(FlagArgument):
239
    """A flag argument with that prints current version"""
240

    
241
    @property
242
    def value(self):
243
        """bool"""
244
        return super(self.__class__, self).value
245

    
246
    @value.setter
247
    def value(self, newvalue):
248
        self._value = newvalue
249
        self.main()
250

    
251
    def main(self):
252
        """Print current version"""
253
        if self.value:
254
            import kamaki
255
            print('kamaki %s' % kamaki.__version__)
256

    
257

    
258
class KeyValueArgument(Argument):
259
    """A Value Argument that can be repeated
260

261
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
262
    """
263

    
264
    def __init__(self, help='', parsed_name=None, default=[]):
265
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
266

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

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

    
285

    
286
class ProgressBarArgument(FlagArgument):
287
    """Manage a progress bar"""
288

    
289
    def __init__(self, help='', parsed_name='', default=True):
290
        self.suffix = '%(percent)d%%'
291
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
292
        try:
293
            KamakiProgressBar
294
        except NameError:
295
            kloger.warning('no progress bar functionality')
296

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

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

    
319
        def progress_gen(n):
320
            for i in self.bar.iter(range(int(n))):
321
                yield
322
            yield
323
        return progress_gen
324

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

    
333

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

    
348

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

    
359

    
360
class ArgumentParseManager(object):
361
    """Manage (initialize and update) an ArgumentParser object"""
362

    
363
    parser = None
364
    _arguments = {}
365
    _parser_modified = False
366
    _parsed = None
367
    _unparsed = None
368

    
369
    def __init__(self, exe, arguments=None):
370
        """
371
        :param exe: (str) the basic command (e.g. 'kamaki')
372

373
        :param arguments: (dict) if given, overrides the global _argument as
374
            the parsers arguments specification
375
        """
376
        self.parser = ArgumentParser(add_help=False)
377
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
378
        if arguments:
379
            self.arguments = arguments
380
        else:
381
            global _arguments
382
            self.arguments = _arguments
383
        self.parse()
384

    
385
    @property
386
    def syntax(self):
387
        """The command syntax (useful for help messages, descriptions, etc)"""
388
        return self.parser.prog
389

    
390
    @syntax.setter
391
    def syntax(self, new_syntax):
392
        self.parser.prog = new_syntax
393

    
394
    @property
395
    def arguments(self):
396
        """(dict) arguments the parser should be aware of"""
397
        return self._arguments
398

    
399
    @arguments.setter
400
    def arguments(self, new_arguments):
401
        if new_arguments:
402
            assert isinstance(new_arguments, dict)
403
        self._arguments = new_arguments
404
        self.update_parser()
405

    
406
    @property
407
    def parsed(self):
408
        """(Namespace) parser-matched terms"""
409
        if self._parser_modified:
410
            self.parse()
411
        return self._parsed
412

    
413
    @property
414
    def unparsed(self):
415
        """(list) parser-unmatched terms"""
416
        if self._parser_modified:
417
            self.parse()
418
        return self._unparsed
419

    
420
    def update_parser(self, arguments=None):
421
        """Load argument specifications to parser
422

423
        :param arguments: if not given, update self.arguments instead
424
        """
425
        if not arguments:
426
            arguments = self._arguments
427

    
428
        for name, arg in arguments.items():
429
            try:
430
                arg.update_parser(self.parser, name)
431
                self._parser_modified = True
432
            except ArgumentError:
433
                pass
434

    
435
    def update_arguments(self, new_arguments):
436
        """Add to / update existing arguments
437

438
        :param new_arguments: (dict)
439
        """
440
        if new_arguments:
441
            assert isinstance(new_arguments, dict)
442
            self._arguments.update(new_arguments)
443
            self.update_parser()
444

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