Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument / __init__.py @ 023d5ada

History | View | Annotate | Download (25.3 kB)

1
# Copyright 2012-2013 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 (
36
    CLISyntaxError, raiseCLIError, CLIInvalidArgument)
37
from kamaki.cli.utils import split_input, to_bytes
38

    
39
from datetime import datetime as dtm
40
from time import mktime
41
from sys import stderr
42

    
43
from logging import getLogger
44
from argparse import (
45
    ArgumentParser, ArgumentError, RawDescriptionHelpFormatter)
46
from progress.bar import ShadyBar as KamakiProgressBar
47

    
48
log = getLogger(__name__)
49

    
50

    
51
class NoAbbrArgumentParser(ArgumentParser):
52
    """This is Argument Parser with disabled argument abbreviation"""
53

    
54
    def _get_option_tuples(self, option_string):
55
        result = []
56
        chars = self.prefix_chars
57
        if option_string[0] in chars and option_string[1] in chars:
58
            if '=' in option_string:
59
                option_prefix, explicit_arg = option_string.split('=', 1)
60
            else:
61
                option_prefix = option_string
62
                explicit_arg = None
63
            for option_string in self._option_string_actions:
64
                if option_string == option_prefix:
65
                    action = self._option_string_actions[option_string]
66
                    tup = action, option_string, explicit_arg
67
                    result.append(tup)
68
        elif option_string[0] in chars and option_string[1] not in chars:
69
            option_prefix = option_string
70
            explicit_arg = None
71
            short_option_prefix = option_string[:2]
72
            short_explicit_arg = option_string[2:]
73

    
74
            for option_string in self._option_string_actions:
75
                if option_string == short_option_prefix:
76
                    action = self._option_string_actions[option_string]
77
                    tup = action, option_string, short_explicit_arg
78
                    result.append(tup)
79
                elif option_string == option_prefix:
80
                    action = self._option_string_actions[option_string]
81
                    tup = action, option_string, explicit_arg
82
                    result.append(tup)
83
        else:
84
            return super(
85
                NoAbbrArgumentParser, self)._get_option_tuples(option_string)
86
        return result
87

    
88

    
89
class Argument(object):
90
    """An argument that can be parsed from command line or otherwise.
91
    This is the top-level Argument class. It is suggested to extent this
92
    class into more specific argument types.
93
    """
94
    lvalue_delimiter = '/'
95

    
96
    def __init__(self, arity, help=None, parsed_name=None, default=None):
97
        self.arity = int(arity)
98
        self.help = '%s' % help or ''
99

    
100
        assert parsed_name, 'No parsed name for argument %s' % self
101
        self.parsed_name = list(parsed_name) if isinstance(
102
            parsed_name, list) or isinstance(parsed_name, tuple) else (
103
                '%s' % parsed_name).split()
104
        for name in self.parsed_name:
105
            assert name.count(' ') == 0, '%s: Invalid parse name "%s"' % (
106
                self, name)
107
            msg = '%s: Invalid parse name "%s" should start with a "-"' % (
108
                    self, name)
109
            assert name.startswith('-'), msg
110

    
111
        self.default = default or None
112

    
113
    @property
114
    def value(self):
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 else (
124
            'store' if self.arity else 'store_true')
125
        parser.add_argument(
126
            *self.parsed_name,
127
            dest=name, action=action, default=self.default, help=self.help)
128

    
129
    @property
130
    def lvalue(self):
131
        """A printable form of the left value when calling an argument e.g.,
132
        --left-value=right-value"""
133
        return (self.lvalue_delimiter or ' ').join(self.parsed_name or [])
134

    
135

    
136
class ConfigArgument(Argument):
137
    """Manage a kamaki configuration (file)"""
138

    
139
    def __init__(self, help, parsed_name=('-c', '--config')):
140
        super(ConfigArgument, self).__init__(1, help, parsed_name, None)
141
        self.file_path = None
142

    
143
    @property
144
    def value(self):
145
        return getattr(self, '_value', None)
146

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

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

    
161
    @property
162
    def groups(self):
163
        suffix = '_cli'
164
        slen = len(suffix)
165
        return [term[:-slen] for term in self.value.keys('global') if (
166
            term.endswith(suffix))]
167

    
168
    @property
169
    def cli_specs(self):
170
        suffix = '_cli'
171
        slen = len(suffix)
172
        return [(k[:-slen], v) for k, v in self.value.items('global') if (
173
            k.endswith(suffix))]
174

    
175
    def get_global(self, option):
176
        return self.value.get('global', option)
177

    
178
    def get_cloud(self, cloud, option):
179
        return self.value.get_cloud(cloud, option)
180

    
181

    
182
_config_arg = ConfigArgument('Path to config file')
183

    
184

    
185
class RuntimeConfigArgument(Argument):
186
    """Set a run-time setting option (not persistent)"""
187

    
188
    def __init__(self, config_arg, help='', parsed_name=None, default=None):
189
        super(self.__class__, self).__init__(1, help, parsed_name, default)
190
        self._config_arg = config_arg
191

    
192
    @property
193
    def value(self):
194
        return super(RuntimeConfigArgument, self).value
195

    
196
    @value.setter
197
    def value(self, options):
198
        if options == self.default:
199
            return
200
        if not isinstance(options, list):
201
            options = ['%s' % options]
202
        for option in options:
203
            keypath, sep, val = option.partition('=')
204
            if not sep:
205
                raiseCLIError(
206
                    CLISyntaxError('Argument Syntax Error '),
207
                    details=[
208
                        '%s is missing a "="',
209
                        ' (usage: -o section.key=val)' % option])
210
            section, sep, key = keypath.partition('.')
211
        if not sep:
212
            key = section
213
            section = 'global'
214
        self._config_arg.value.override(
215
            section.strip(),
216
            key.strip(),
217
            val.strip())
218

    
219

    
220
class FlagArgument(Argument):
221
    """
222
    :value: true if set, false otherwise
223
    """
224

    
225
    def __init__(self, help='', parsed_name=None, default=None):
226
        super(FlagArgument, self).__init__(0, help, parsed_name, default)
227

    
228

    
229
class ValueArgument(Argument):
230
    """
231
    :value type: string
232
    :value returns: given value or default
233
    """
234

    
235
    def __init__(self, help='', parsed_name=None, default=None):
236
        super(ValueArgument, self).__init__(1, help, parsed_name, default)
237

    
238

    
239
class CommaSeparatedListArgument(ValueArgument):
240
    """
241
    :value type: string
242
    :value returns: list of the comma separated values
243
    """
244

    
245
    @property
246
    def value(self):
247
        return self._value or list()
248

    
249
    @value.setter
250
    def value(self, newvalue):
251
        self._value = newvalue.split(',') if newvalue else list()
252

    
253

    
254
class IntArgument(ValueArgument):
255

    
256
    @property
257
    def value(self):
258
        """integer (type checking)"""
259
        return getattr(self, '_value', self.default)
260

    
261
    @value.setter
262
    def value(self, newvalue):
263
        if newvalue == self.default:
264
            self._value = newvalue
265
            return
266
        try:
267
            if int(newvalue) == float(newvalue):
268
                self._value = int(newvalue)
269
            else:
270
                raise ValueError('Raise int argument error')
271
        except ValueError:
272
            raiseCLIError(CLISyntaxError(
273
                'IntArgument Error',
274
                details=['Value %s not an int' % newvalue]))
275

    
276

    
277
class DataSizeArgument(ValueArgument):
278
    """Input: a string of the form <number><unit>
279
    Output: the number of bytes
280
    Units: B, KiB, KB, MiB, MB, GiB, GB, TiB, TB
281
    """
282

    
283
    @property
284
    def value(self):
285
        return getattr(self, '_value', self.default)
286

    
287
    def _calculate_limit(self, user_input):
288
        limit = 0
289
        try:
290
            limit = int(user_input)
291
        except ValueError:
292
            index = 0
293
            digits = [str(num) for num in range(0, 10)] + ['.']
294
            while user_input[index] in digits:
295
                index += 1
296
            limit = user_input[:index]
297
            format = user_input[index:]
298
            try:
299
                return to_bytes(limit, format)
300
            except Exception as qe:
301
                msg = 'Failed to convert %s to bytes' % user_input,
302
                raiseCLIError(qe, msg, details=[
303
                    'Syntax: containerlimit set <limit>[format] [container]',
304
                    'e.g.,: containerlimit set 2.3GB mycontainer',
305
                    'Valid formats:',
306
                    '(*1024): B, KiB, MiB, GiB, TiB',
307
                    '(*1000): B, KB, MB, GB, TB'])
308
        return limit
309

    
310
    @value.setter
311
    def value(self, new_value):
312
        if new_value:
313
            self._value = self._calculate_limit(new_value)
314

    
315

    
316
class UserAccountArgument(ValueArgument):
317
    """A user UUID or name (if uuid does not exist)"""
318

    
319
    account_client = None
320

    
321
    @property
322
    def value(self):
323
        return super(UserAccountArgument, self).value
324

    
325
    @value.setter
326
    def value(self, uuid_or_name):
327
        if uuid_or_name and self.account_client:
328
            r = self.account_client.uuids2usernames([uuid_or_name, ])
329
            if r:
330
                self._value = uuid_or_name
331
            else:
332
                r = self.account_client.usernames2uuids([uuid_or_name])
333
                self._value = r.get(uuid_or_name) if r else None
334
            if not self._value:
335
                raise raiseCLIError('User name or UUID not found', details=[
336
                    '%s is not a known username or UUID' % uuid_or_name,
337
                    'Usage:  %s <USER_UUID | USERNAME>' % self.lvalue])
338

    
339

    
340
class DateArgument(ValueArgument):
341

    
342
    DATE_FORMAT = '%a %b %d %H:%M:%S %Y'
343

    
344
    INPUT_FORMATS = [DATE_FORMAT, '%d-%m-%Y', '%H:%M:%S %d-%m-%Y']
345

    
346
    @property
347
    def timestamp(self):
348
        v = getattr(self, '_value', self.default)
349
        return mktime(v.timetuple()) if v else None
350

    
351
    @property
352
    def formated(self):
353
        v = getattr(self, '_value', self.default)
354
        return v.strftime(self.DATE_FORMAT) if v else None
355

    
356
    @property
357
    def value(self):
358
        return self.timestamp
359

    
360
    @value.setter
361
    def value(self, newvalue):
362
        self._value = self.format_date(newvalue) if newvalue else self.default
363

    
364
    def format_date(self, datestr):
365
        for format in self.INPUT_FORMATS:
366
            try:
367
                t = dtm.strptime(datestr, format)
368
            except ValueError:
369
                continue
370
            return t  # .strftime(self.DATE_FORMAT)
371
        raiseCLIError(None, 'Date Argument Error', details=[
372
            '%s not a valid date' % datestr,
373
            'Correct formats:\n\t%s' % self.INPUT_FORMATS])
374

    
375

    
376
class VersionArgument(FlagArgument):
377
    """A flag argument with that prints current version"""
378

    
379
    @property
380
    def value(self):
381
        """bool"""
382
        return super(self.__class__, self).value
383

    
384
    @value.setter
385
    def value(self, newvalue):
386
        self._value = newvalue
387
        if newvalue:
388
            import kamaki
389
            print('kamaki %s' % kamaki.__version__)
390

    
391

    
392
class RepeatableArgument(Argument):
393
    """A value argument that can be repeated"""
394

    
395
    def __init__(self, help='', parsed_name=None, default=None):
396
        super(RepeatableArgument, self).__init__(
397
            -1, help, parsed_name, default)
398

    
399
    @property
400
    def value(self):
401
        return getattr(self, '_value', [])
402

    
403
    @value.setter
404
    def value(self, newvalue):
405
        self._value = newvalue
406

    
407

    
408
class KeyValueArgument(Argument):
409
    """A Key=Value Argument that can be repeated
410

411
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
412
    """
413

    
414
    def __init__(self, help='', parsed_name=None, default=None):
415
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
416

    
417
    @property
418
    def value(self):
419
        """
420
        :returns: (dict) {key1: val1, key2: val2, ...}
421
        """
422
        return getattr(self, '_value', {})
423

    
424
    @value.setter
425
    def value(self, keyvalue_pairs):
426
        """
427
        :param keyvalue_pairs: (str) ['key1=val1', 'key2=val2', ...]
428
        """
429
        if keyvalue_pairs:
430
            self._value = self.value
431
            try:
432
                for pair in keyvalue_pairs:
433
                    key, sep, val = pair.partition('=')
434
                    assert sep, ' %s misses a "=" (usage: key1=val1 )\n' % (
435
                        pair)
436
                    self._value[key] = val
437
            except Exception as e:
438
                raiseCLIError(e, 'KeyValueArgument Syntax Error')
439

    
440

    
441
class StatusArgument(ValueArgument):
442
    """Initialize with valid_states=['list', 'of', 'states']
443
    First state is the default"""
444

    
445
    def __init__(self, *args, **kwargs):
446
        self.valid_states = kwargs.pop('valid_states', ['BUILD', ])
447
        super(StatusArgument, self).__init__(*args, **kwargs)
448

    
449
    @property
450
    def value(self):
451
        return getattr(self, '_value', None)
452

    
453
    @value.setter
454
    def value(self, new_status):
455
        if new_status:
456
            new_status = new_status.upper()
457
            if new_status not in self.valid_states:
458
                raise CLIInvalidArgument(
459
                    'Invalid argument %s' % new_status, details=[
460
                    'Usage: '
461
                    '%s=[%s]' % (self.lvalue, '|'.join(self.valid_states))])
462
            self._value = new_status
463

    
464

    
465
class ProgressBarArgument(FlagArgument):
466
    """Manage a progress bar"""
467

    
468
    def __init__(self, help='', parsed_name='', default=True):
469
        self.suffix = '%(percent)d%%'
470
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
471

    
472
    def clone(self):
473
        """Get a modifiable copy of this bar"""
474
        newarg = ProgressBarArgument(
475
            self.help, self.parsed_name, self.default)
476
        newarg._value = self._value
477
        return newarg
478

    
479
    def get_generator(
480
            self, message, message_len=25, countdown=False, timeout=100):
481
        """Get a generator to handle progress of the bar (gen.next())"""
482
        if self.value:
483
            return None
484
        try:
485
            self.bar = KamakiProgressBar(
486
                message.ljust(message_len), max=timeout or 100)
487
        except NameError:
488
            self.value = None
489
            return self.value
490
        if countdown:
491
            bar_phases = list(self.bar.phases)
492
            self.bar.empty_fill, bar_phases[0] = bar_phases[-1], ''
493
            bar_phases.reverse()
494
            self.bar.phases = bar_phases
495
            self.bar.bar_prefix = ' '
496
            self.bar.bar_suffix = ' '
497
            self.bar.suffix = '%(remaining)ds to timeout'
498
        else:
499
            self.bar.suffix = '%(percent)d%% - %(eta)ds'
500
        self.bar.start()
501

    
502
        def progress_gen(n):
503
            for i in self.bar.iter(range(int(n))):
504
                yield
505
            yield
506
        return progress_gen
507

    
508
    def finish(self):
509
        """Stop progress bar, return terminal cursor to user"""
510
        if self.value:
511
            return
512
        mybar = getattr(self, 'bar', None)
513
        if mybar:
514
            mybar.finish()
515

    
516

    
517
_arguments = dict(
518
    config=_config_arg,
519
    cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
520
    help=Argument(0, 'Show help message', ('-h', '--help')),
521
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
522
    #include=FlagArgument(
523
    #    'Include raw connection data in the output', ('-i', '--include')),
524
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
525
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
526
    version=VersionArgument('Print current version', ('-V', '--version')),
527
    options=RuntimeConfigArgument(
528
        _config_arg, 'Override a config value', ('-o', '--options'))
529
)
530

    
531

    
532
#  Initial command line interface arguments
533

    
534

    
535
class ArgumentParseManager(object):
536
    """Manage (initialize and update) an ArgumentParser object"""
537

    
538
    def __init__(
539
            self, exe,
540
            arguments=None, required=None, syntax=None, description=None,
541
            check_required=True):
542
        """
543
        :param exe: (str) the basic command (e.g. 'kamaki')
544

545
        :param arguments: (dict) if given, overrides the global _argument as
546
            the parsers arguments specification
547
        :param required: (list or tuple) an iterable of argument keys, denoting
548
            which arguments are required. A tuple denoted an AND relation,
549
            while a list denotes an OR relation e.g., ['a', 'b'] means that
550
            either 'a' or 'b' is required, while ('a', 'b') means that both 'a'
551
            and 'b' ar required.
552
            Nesting is allowed e.g., ['a', ('b', 'c'), ['d', 'e']] means that
553
            this command required either 'a', or both 'b' and 'c', or one of
554
            'd', 'e'.
555
            Repeated arguments are also allowed e.g., [('a', 'b'), ('a', 'c'),
556
            ['b', 'c']] means that the command required either 'a' and 'b' or
557
            'a' and 'c' or at least one of 'b', 'c' and could be written as
558
            [('a', ['b', 'c']), ['b', 'c']]
559
        :param syntax: (str) The basic syntax of the arguments. Default:
560
            exe <cmd_group> [<cmd_subbroup> ...] <cmd>
561
        :param description: (str) The description of the commands or ''
562
        :param check_required: (bool) Set to False inorder not to check for
563
            required argument values while parsing
564
        """
565
        self.parser = NoAbbrArgumentParser(
566
            add_help=False, formatter_class=RawDescriptionHelpFormatter)
567
        self._exe = exe
568
        self.syntax = syntax or (
569
            '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe)
570
        self.required, self.check_required = required, check_required
571
        self.parser.description = description or ''
572
        if arguments:
573
            self.arguments = arguments
574
        else:
575
            global _arguments
576
            self.arguments = _arguments
577
        self._parser_modified, self._parsed, self._unparsed = False, None, None
578
        self.parse()
579

    
580
    @staticmethod
581
    def required2list(required):
582
        if isinstance(required, list) or isinstance(required, tuple):
583
            terms = []
584
            for r in required:
585
                terms.append(ArgumentParseManager.required2list(r))
586
            return list(set(terms).union())
587
        return required
588

    
589
    @staticmethod
590
    def required2str(required, arguments, tab=''):
591
        if isinstance(required, list):
592
            return ' %sat least one of the following:\n%s' % (tab, ''.join(
593
                [ArgumentParseManager.required2str(
594
                    r, arguments, tab + '  ') for r in required]))
595
        elif isinstance(required, tuple):
596
            return ' %sall of the following:\n%s' % (tab, ''.join(
597
                [ArgumentParseManager.required2str(
598
                    r, arguments, tab + '  ') for r in required]))
599
        else:
600
            lt_pn, lt_all, arg = 23, 80, arguments[required]
601
            tab2 = ' ' * lt_pn
602
            ret = '%s%s' % (tab, ', '.join(arg.parsed_name))
603
            if arg.arity != 0:
604
                ret += ' %s' % required.upper()
605
            ret = ('{:<%s}' % lt_pn).format(ret)
606
            prefix = ('\n%s' % tab2) if len(ret) > lt_pn else ' '
607
            cur = 0
608
            while arg.help[cur:]:
609
                next = cur + lt_all - lt_pn
610
                ret += prefix
611
                ret += ('{:<%s}' % (lt_all - lt_pn)).format(arg.help[cur:next])
612
                cur, finish = next, '\n%s' % tab2
613
            return ret + '\n'
614

    
615
    @staticmethod
616
    def _patch_with_required_args(arguments, required):
617
        if isinstance(required, tuple):
618
            return ' '.join([ArgumentParseManager._patch_with_required_args(
619
                arguments, k) for k in required])
620
        elif isinstance(required, list):
621
            return '< %s >' % ' | '.join([
622
                ArgumentParseManager._patch_with_required_args(
623
                    arguments, k) for k in required])
624
        arg = arguments[required]
625
        return '/'.join(arg.parsed_name) + (
626
            ' %s [...]' % required.upper() if arg.arity < 0 else (
627
                ' %s' % required.upper() if arg.arity else ''))
628

    
629
    def print_help(self, out=stderr):
630
        if self.required:
631
            tmp_args = dict(self.arguments)
632
            for term in self.required2list(self.required):
633
                tmp_args.pop(term)
634
            tmp_parser = ArgumentParseManager(self._exe, tmp_args)
635
            tmp_parser.syntax = self.syntax + self._patch_with_required_args(
636
                self.arguments, self.required)
637
            tmp_parser.parser.description = '%s\n\nrequired arguments:\n%s' % (
638
                self.parser.description,
639
                self.required2str(self.required, self.arguments))
640
            tmp_parser.update_parser()
641
            tmp_parser.parser.print_help()
642
        else:
643
            self.parser.print_help()
644

    
645
    @property
646
    def syntax(self):
647
        """The command syntax (useful for help messages, descriptions, etc)"""
648
        return self.parser.prog
649

    
650
    @syntax.setter
651
    def syntax(self, new_syntax):
652
        self.parser.prog = new_syntax
653

    
654
    @property
655
    def arguments(self):
656
        """:returns: (dict) arguments the parser should be aware of"""
657
        return self._arguments
658

    
659
    @arguments.setter
660
    def arguments(self, new_arguments):
661
        assert isinstance(new_arguments, dict), 'Arguments must be in a dict'
662
        self._arguments = new_arguments
663
        self.update_parser()
664

    
665
    @property
666
    def parsed(self):
667
        """(Namespace) parser-matched terms"""
668
        if self._parser_modified:
669
            self.parse()
670
        return self._parsed
671

    
672
    @property
673
    def unparsed(self):
674
        """(list) parser-unmatched terms"""
675
        if self._parser_modified:
676
            self.parse()
677
        return self._unparsed
678

    
679
    def update_parser(self, arguments=None):
680
        """Load argument specifications to parser
681

682
        :param arguments: if not given, update self.arguments instead
683
        """
684
        arguments = arguments or self._arguments
685

    
686
        for name, arg in arguments.items():
687
            try:
688
                arg.update_parser(self.parser, name)
689
                self._parser_modified = True
690
            except ArgumentError:
691
                pass
692

    
693
    def update_arguments(self, new_arguments):
694
        """Add to / update existing arguments
695

696
        :param new_arguments: (dict)
697
        """
698
        if new_arguments:
699
            assert isinstance(new_arguments, dict), 'Arguments not in dict !!!'
700
            self._arguments.update(new_arguments)
701
            self.update_parser()
702

    
703
    def _parse_required_arguments(self, required, parsed_args):
704
        if not (self.check_required and required):
705
            return True
706
        if isinstance(required, tuple):
707
            for item in required:
708
                if not self._parse_required_arguments(item, parsed_args):
709
                    return False
710
            return True
711
        elif isinstance(required, list):
712
            for item in required:
713
                if self._parse_required_arguments(item, parsed_args):
714
                    return True
715
            return False
716
        return required in parsed_args
717

    
718
    def parse(self, new_args=None):
719
        """Parse user input"""
720
        try:
721
            pkargs = (new_args,) if new_args else ()
722
            self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
723
            parsed_args = [
724
                k for k, v in vars(self._parsed).items() if v not in (None, )]
725
            if not self._parse_required_arguments(self.required, parsed_args):
726
                self.print_help()
727
                raise CLISyntaxError('Missing required arguments')
728
        except SystemExit:
729
            raiseCLIError(CLISyntaxError('Argument Syntax Error'))
730
        for name, arg in self.arguments.items():
731
            arg.value = getattr(self._parsed, name, arg.default)
732
        self._unparsed = []
733
        for term in unparsed:
734
            self._unparsed += split_input(' \'%s\' ' % term)
735
        self._parser_modified = False