__init__.py 26.4 KB
Newer Older
1
# Copyright 2012-2014 GRNET S.A. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#     copyright notice, this list of conditions and the following
#     disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#     copyright notice, this list of conditions and the following
#     disclaimer in the documentation and/or other materials
#     provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
33

34
from kamaki.cli.config import Config
35 36
from kamaki.cli.errors import (
    CLISyntaxError, raiseCLIError, CLIInvalidArgument)
37
from kamaki.cli.utils import split_input, to_bytes
38

39
from datetime import datetime as dtm
40 41
import dateutil.tz
import dateutil.parser
42
from time import mktime
43
from sys import stderr
Stavros Sachtouris's avatar
Stavros Sachtouris committed
44

45
from logging import getLogger
46 47
from argparse import (
    ArgumentParser, ArgumentError, RawDescriptionHelpFormatter)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
48
from progress.bar import ShadyBar as KamakiProgressBar
49

50
log = getLogger(__name__)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
51

52

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
class NoAbbrArgumentParser(ArgumentParser):
    """This is Argument Parser with disabled argument abbreviation"""

    def _get_option_tuples(self, option_string):
        result = []
        chars = self.prefix_chars
        if option_string[0] in chars and option_string[1] in chars:
            if '=' in option_string:
                option_prefix, explicit_arg = option_string.split('=', 1)
            else:
                option_prefix = option_string
                explicit_arg = None
            for option_string in self._option_string_actions:
                if option_string == option_prefix:
                    action = self._option_string_actions[option_string]
                    tup = action, option_string, explicit_arg
                    result.append(tup)
        elif option_string[0] in chars and option_string[1] not in chars:
            option_prefix = option_string
            explicit_arg = None
            short_option_prefix = option_string[:2]
            short_explicit_arg = option_string[2:]

            for option_string in self._option_string_actions:
                if option_string == short_option_prefix:
                    action = self._option_string_actions[option_string]
                    tup = action, option_string, short_explicit_arg
                    result.append(tup)
                elif option_string == option_prefix:
                    action = self._option_string_actions[option_string]
                    tup = action, option_string, explicit_arg
                    result.append(tup)
        else:
            return super(
                NoAbbrArgumentParser, self)._get_option_tuples(option_string)
        return result


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

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

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

113
        self.default = default or None
114

115
    @property
116 117
    def value(self):
        return getattr(self, '_value', self.default)
118

119 120 121 122 123
    @value.setter
    def value(self, newvalue):
        self._value = newvalue

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

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

137

138
class ConfigArgument(Argument):
139
    """Manage a kamaki configuration (file)"""
140

141 142 143
    def __init__(self, help, parsed_name=('-c', '--config')):
        super(ConfigArgument, self).__init__(1, help, parsed_name, None)
        self.file_path = None
Stavros Sachtouris's avatar
Stavros Sachtouris committed
144

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

149 150
    @value.setter
    def value(self, config_file):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
151 152
        if config_file:
            self._value = Config(config_file)
153 154 155
            self.file_path = config_file
        elif self.file_path:
            self._value = Config(self.file_path)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
156 157
        else:
            self._value = Config()
158

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

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

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

    def get_global(self, option):
178
        return self.value.get('global', option)
179

180 181
    def get_cloud(self, cloud, option):
        return self.value.get_cloud(cloud, option)
182

183

184
_config_arg = ConfigArgument('Path to config file')
185

186

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

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

194
    @property
195
    def value(self):
196
        return super(RuntimeConfigArgument, self).value
197

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

221 222

class FlagArgument(Argument):
223
    """
224
    :value: true if set, false otherwise
225 226
    """

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

230

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

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

240

Stavros Sachtouris's avatar
Stavros Sachtouris committed
241 242 243 244 245 246 247 248 249 250
class BooleanArgument(ValueArgument):
    """Can be true, false or None (Flag argument can only be true or None)"""

    @property
    def value(self):
        return getattr(self, '_value', None)

    @value.setter
    def value(self, new_value):
        if new_value:
251 252
            v = new_value.lower()
            if v not in ('true', 'false'):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
253
                raise CLIInvalidArgument(
254
                    'Invalid value %s=%s' % (self.lvalue, new_value), details=[
Stavros Sachtouris's avatar
Stavros Sachtouris committed
255
                    'Usage:', '%s=<true|false>' % self.lvalue])
256
            self._value = bool(v == 'true')
Stavros Sachtouris's avatar
Stavros Sachtouris committed
257 258


259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
class CommaSeparatedListArgument(ValueArgument):
    """
    :value type: string
    :value returns: list of the comma separated values
    """

    @property
    def value(self):
        return self._value or list()

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


274
class IntArgument(ValueArgument):
275

276
    @property
277
    def value(self):
278
        """integer (type checking)"""
279
        return getattr(self, '_value', self.default)
280

281 282
    @value.setter
    def value(self, newvalue):
283 284 285
        if newvalue == self.default:
            self._value = newvalue
            return
286
        try:
287 288 289 290
            if int(newvalue) == float(newvalue):
                self._value = int(newvalue)
            else:
                raise ValueError('Raise int argument error')
291
        except ValueError:
292 293
            raiseCLIError(CLISyntaxError(
                'IntArgument Error',
294
                details=['Value %s not an int' % newvalue]))
295

296

297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
class DataSizeArgument(ValueArgument):
    """Input: a string of the form <number><unit>
    Output: the number of bytes
    Units: B, KiB, KB, MiB, MB, GiB, GB, TiB, TB
    """

    @property
    def value(self):
        return getattr(self, '_value', self.default)

    def _calculate_limit(self, user_input):
        limit = 0
        try:
            limit = int(user_input)
        except ValueError:
            index = 0
            digits = [str(num) for num in range(0, 10)] + ['.']
            while user_input[index] in digits:
                index += 1
            limit = user_input[:index]
            format = user_input[index:]
            try:
                return to_bytes(limit, format)
            except Exception as qe:
                msg = 'Failed to convert %s to bytes' % user_input,
                raiseCLIError(qe, msg, details=[
                    'Syntax: containerlimit set <limit>[format] [container]',
                    'e.g.,: containerlimit set 2.3GB mycontainer',
                    'Valid formats:',
                    '(*1024): B, KiB, MiB, GiB, TiB',
                    '(*1000): B, KB, MB, GB, TB'])
        return limit

    @value.setter
    def value(self, new_value):
        if new_value:
            self._value = self._calculate_limit(new_value)


336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
class UserAccountArgument(ValueArgument):
    """A user UUID or name (if uuid does not exist)"""

    account_client = None

    @property
    def value(self):
        return super(UserAccountArgument, self).value

    @value.setter
    def value(self, uuid_or_name):
        if uuid_or_name and self.account_client:
            r = self.account_client.uuids2usernames([uuid_or_name, ])
            if r:
                self._value = uuid_or_name
            else:
                r = self.account_client.usernames2uuids([uuid_or_name])
                self._value = r.get(uuid_or_name) if r else None
            if not self._value:
                raise raiseCLIError('User name or UUID not found', details=[
                    '%s is not a known username or UUID' % uuid_or_name,
                    'Usage:  %s <USER_UUID | USERNAME>' % self.lvalue])


360 361
class DateArgument(ValueArgument):

362 363 364 365
    DATE_FORMATS = ['%a %b %d %H:%M:%S %Y', '%d-%m-%Y', '%H:%M:%S %d-%m-%Y']
    INPUT_FORMATS = [
        'YYYY-mm-dd', '"HH:MM:SS YYYY-mm-dd"', 'YYYY-mm-ddTHH:MM:SS+TMZ',
        '"Day Mon dd HH:MM:SS YYYY"']
366

367 368 369 370 371 372 373 374
    @property
    def timestamp(self):
        v = getattr(self, '_value', self.default)
        return mktime(v.timetuple()) if v else None

    @property
    def formated(self):
        v = getattr(self, '_value', self.default)
375
        return v.strftime(self.DATE_FORMATS[0]) if v else None
376

377 378
    @property
    def value(self):
379
        return self.timestamp
380

381 382
    @property
    def isoformat(self):
383 384 385 386 387 388
        d = getattr(self, '_value', self.default)
        if not d:
            return None
        if not d.tzinfo:
            d = d.replace(tzinfo=dateutil.tz.tzlocal())
        return d.isoformat()
389

390 391
    @value.setter
    def value(self, newvalue):
392 393 394 395 396 397 398 399
        if newvalue:
            try:
                self._value = dateutil.parser.parse(newvalue)
            except Exception:
                raise CLIInvalidArgument(
                    'Invalid value "%s" for date argument %s' % (
                        newvalue, self.lvalue),
                    details=['Suggested formats:'] + self.INPUT_FORMATS)
400 401

    def format_date(self, datestr):
402
        for fmt in self.DATE_FORMATS:
403
            try:
404 405
                return dtm.strptime(datestr, fmt)
            except ValueError as ve:
406
                continue
407 408
        raise raiseCLIError(ve, 'Failed to format date', details=[
            '%s could not be formated for HTTP headers' % datestr])
409 410


411
class VersionArgument(FlagArgument):
412 413
    """A flag argument with that prints current version"""

414
    @property
415
    def value(self):
416
        """bool"""
417
        return super(self.__class__, self).value
418

419 420 421
    @value.setter
    def value(self, newvalue):
        self._value = newvalue
422
        if newvalue:
423
            import kamaki
424 425
            print('kamaki %s' % kamaki.__version__)

426

427 428 429
class RepeatableArgument(Argument):
    """A value argument that can be repeated"""

430
    def __init__(self, help='', parsed_name=None, default=None):
431 432 433
        super(RepeatableArgument, self).__init__(
            -1, help, parsed_name, default)

434 435 436 437 438 439 440 441
    @property
    def value(self):
        return getattr(self, '_value', [])

    @value.setter
    def value(self, newvalue):
        self._value = newvalue

442

443
class KeyValueArgument(Argument):
444
    """A Key=Value Argument that can be repeated
445

446
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
447 448
    """

449
    def __init__(self, help='', parsed_name=None, default=None):
450
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
451

452
    @property
453
    def value(self):
454
        """
455
        :returns: (dict) {key1: val1, key2: val2, ...}
456
        """
457
        return getattr(self, '_value', {})
458 459

    @value.setter
460
    def value(self, keyvalue_pairs):
461 462 463
        """
        :param keyvalue_pairs: (str) ['key1=val1', 'key2=val2', ...]
        """
464 465 466 467 468 469 470 471 472 473
        if keyvalue_pairs:
            self._value = self.value
            try:
                for pair in keyvalue_pairs:
                    key, sep, val = pair.partition('=')
                    assert sep, ' %s misses a "=" (usage: key1=val1 )\n' % (
                        pair)
                    self._value[key] = val
            except Exception as e:
                raiseCLIError(e, 'KeyValueArgument Syntax Error')
474

475

476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
class StatusArgument(ValueArgument):
    """Initialize with valid_states=['list', 'of', 'states']
    First state is the default"""

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

    @property
    def value(self):
        return getattr(self, '_value', None)

    @value.setter
    def value(self, new_status):
        if new_status:
            new_status = new_status.upper()
            if new_status not in self.valid_states:
                raise CLIInvalidArgument(
                    'Invalid argument %s' % new_status, details=[
                    'Usage: '
                    '%s=[%s]' % (self.lvalue, '|'.join(self.valid_states))])
            self._value = new_status


500
class ProgressBarArgument(FlagArgument):
501
    """Manage a progress bar"""
502 503 504 505 506

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

507
    def clone(self):
508
        """Get a modifiable copy of this bar"""
509
        newarg = ProgressBarArgument(
Stavros Sachtouris's avatar
Stavros Sachtouris committed
510
            self.help, self.parsed_name, self.default)
511 512 513
        newarg._value = self._value
        return newarg

514 515
    def get_generator(
            self, message, message_len=25, countdown=False, timeout=100):
516
        """Get a generator to handle progress of the bar (gen.next())"""
517 518 519
        if self.value:
            return None
        try:
Stavros Sachtouris's avatar
Stavros Sachtouris committed
520 521
            self.bar = KamakiProgressBar(
                message.ljust(message_len), max=timeout or 100)
522
        except NameError:
523 524
            self.value = None
            return self.value
525
        if countdown:
526
            bar_phases = list(self.bar.phases)
527 528
            self.bar.empty_fill, bar_phases[0] = bar_phases[-1], ''
            bar_phases.reverse()
529
            self.bar.phases = bar_phases
530
            self.bar.bar_prefix = ' '
531
            self.bar.bar_suffix = ' '
532
            self.bar.suffix = '%(remaining)ds to timeout'
533 534
        else:
            self.bar.suffix = '%(percent)d%% - %(eta)ds'
Stavros Sachtouris's avatar
Stavros Sachtouris committed
535
        self.bar.start()
536

537 538 539 540 541
        def progress_gen(n):
            for i in self.bar.iter(range(int(n))):
                yield
            yield
        return progress_gen
542

543
    def finish(self):
544
        """Stop progress bar, return terminal cursor to user"""
545 546 547 548 549
        if self.value:
            return
        mybar = getattr(self, 'bar', None)
        if mybar:
            mybar.finish()
550 551


552 553
_arguments = dict(
    config=_config_arg,
554
    cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
555 556
    help=Argument(0, 'Show help message', ('-h', '--help')),
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
557 558
    #include=FlagArgument(
    #    'Include raw connection data in the output', ('-i', '--include')),
559 560 561
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
    version=VersionArgument('Print current version', ('-V', '--version')),
562
    options=RuntimeConfigArgument(
563
        _config_arg, 'Override a config value', ('-o', '--options'))
564
)
565 566 567


#  Initial command line interface arguments
568 569


570
class ArgumentParseManager(object):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
571 572
    """Manage (initialize and update) an ArgumentParser object"""

573 574
    def __init__(
            self, exe,
575 576
            arguments=None, required=None, syntax=None, description=None,
            check_required=True):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
577 578 579 580 581
        """
        :param exe: (str) the basic command (e.g. 'kamaki')

        :param arguments: (dict) if given, overrides the global _argument as
            the parsers arguments specification
582 583 584 585 586 587 588 589 590 591 592 593
        :param required: (list or tuple) an iterable of argument keys, denoting
            which arguments are required. A tuple denoted an AND relation,
            while a list denotes an OR relation e.g., ['a', 'b'] means that
            either 'a' or 'b' is required, while ('a', 'b') means that both 'a'
            and 'b' ar required.
            Nesting is allowed e.g., ['a', ('b', 'c'), ['d', 'e']] means that
            this command required either 'a', or both 'b' and 'c', or one of
            'd', 'e'.
            Repeated arguments are also allowed e.g., [('a', 'b'), ('a', 'c'),
            ['b', 'c']] means that the command required either 'a' and 'b' or
            'a' and 'c' or at least one of 'b', 'c' and could be written as
            [('a', ['b', 'c']), ['b', 'c']]
594 595 596
        :param syntax: (str) The basic syntax of the arguments. Default:
            exe <cmd_group> [<cmd_subbroup> ...] <cmd>
        :param description: (str) The description of the commands or ''
597 598
        :param check_required: (bool) Set to False inorder not to check for
            required argument values while parsing
Stavros Sachtouris's avatar
Stavros Sachtouris committed
599
        """
600
        self.parser = NoAbbrArgumentParser(
601
            add_help=False, formatter_class=RawDescriptionHelpFormatter)
602
        self._exe = exe
603 604
        self.syntax = syntax or (
            '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe)
605
        self.required, self.check_required = required, check_required
606
        self.parser.description = description or ''
Stavros Sachtouris's avatar
Stavros Sachtouris committed
607 608 609 610 611
        if arguments:
            self.arguments = arguments
        else:
            global _arguments
            self.arguments = _arguments
612
        self._parser_modified, self._parsed, self._unparsed = False, None, None
613
        self.parse()
Stavros Sachtouris's avatar
Stavros Sachtouris committed
614

615 616 617 618 619 620 621 622 623 624 625 626
    @staticmethod
    def required2list(required):
        if isinstance(required, list) or isinstance(required, tuple):
            terms = []
            for r in required:
                terms.append(ArgumentParseManager.required2list(r))
            return list(set(terms).union())
        return required

    @staticmethod
    def required2str(required, arguments, tab=''):
        if isinstance(required, list):
627
            return ' %sat least one of the following:\n%s' % (tab, ''.join(
628 629 630
                [ArgumentParseManager.required2str(
                    r, arguments, tab + '  ') for r in required]))
        elif isinstance(required, tuple):
631
            return ' %sall of the following:\n%s' % (tab, ''.join(
632 633 634 635 636 637 638 639 640
                [ArgumentParseManager.required2str(
                    r, arguments, tab + '  ') for r in required]))
        else:
            lt_pn, lt_all, arg = 23, 80, arguments[required]
            tab2 = ' ' * lt_pn
            ret = '%s%s' % (tab, ', '.join(arg.parsed_name))
            if arg.arity != 0:
                ret += ' %s' % required.upper()
            ret = ('{:<%s}' % lt_pn).format(ret)
641 642
            prefix = ('\n%s' % tab2) if len(ret) > lt_pn else ' '
            cur = 0
643
            while arg.help[cur:]:
644
                next = cur + lt_all - lt_pn
645 646 647 648 649
                ret += prefix
                ret += ('{:<%s}' % (lt_all - lt_pn)).format(arg.help[cur:next])
                cur, finish = next, '\n%s' % tab2
            return ret + '\n'

650 651 652 653 654 655 656 657 658 659 660 661 662 663
    @staticmethod
    def _patch_with_required_args(arguments, required):
        if isinstance(required, tuple):
            return ' '.join([ArgumentParseManager._patch_with_required_args(
                arguments, k) for k in required])
        elif isinstance(required, list):
            return '< %s >' % ' | '.join([
                ArgumentParseManager._patch_with_required_args(
                    arguments, k) for k in required])
        arg = arguments[required]
        return '/'.join(arg.parsed_name) + (
            ' %s [...]' % required.upper() if arg.arity < 0 else (
                ' %s' % required.upper() if arg.arity else ''))

664 665 666 667 668 669
    def print_help(self, out=stderr):
        if self.required:
            tmp_args = dict(self.arguments)
            for term in self.required2list(self.required):
                tmp_args.pop(term)
            tmp_parser = ArgumentParseManager(self._exe, tmp_args)
670 671
            tmp_parser.syntax = self.syntax + self._patch_with_required_args(
                self.arguments, self.required)
672 673 674 675 676 677 678 679
            tmp_parser.parser.description = '%s\n\nrequired arguments:\n%s' % (
                self.parser.description,
                self.required2str(self.required, self.arguments))
            tmp_parser.update_parser()
            tmp_parser.parser.print_help()
        else:
            self.parser.print_help()

Stavros Sachtouris's avatar
Stavros Sachtouris committed
680 681
    @property
    def syntax(self):
682
        """The command syntax (useful for help messages, descriptions, etc)"""
Stavros Sachtouris's avatar
Stavros Sachtouris committed
683 684 685 686 687 688
        return self.parser.prog

    @syntax.setter
    def syntax(self, new_syntax):
        self.parser.prog = new_syntax

689 690
    @property
    def arguments(self):
691
        """:returns: (dict) arguments the parser should be aware of"""
692 693 694 695
        return self._arguments

    @arguments.setter
    def arguments(self, new_arguments):
696
        assert isinstance(new_arguments, dict), 'Arguments must be in a dict'
697 698 699
        self._arguments = new_arguments
        self.update_parser()

700
    @property
701
    def parsed(self):
702
        """(Namespace) parser-matched terms"""
703 704 705 706 707 708 709 710 711 712 713
        if self._parser_modified:
            self.parse()
        return self._parsed

    @property
    def unparsed(self):
        """(list) parser-unmatched terms"""
        if self._parser_modified:
            self.parse()
        return self._unparsed

Stavros Sachtouris's avatar
Stavros Sachtouris committed
714 715 716 717 718
    def update_parser(self, arguments=None):
        """Load argument specifications to parser

        :param arguments: if not given, update self.arguments instead
        """
719
        arguments = arguments or self._arguments
Stavros Sachtouris's avatar
Stavros Sachtouris committed
720 721 722 723

        for name, arg in arguments.items():
            try:
                arg.update_parser(self.parser, name)
724
                self._parser_modified = True
Stavros Sachtouris's avatar
Stavros Sachtouris committed
725 726 727
            except ArgumentError:
                pass

728 729 730 731 732 733
    def update_arguments(self, new_arguments):
        """Add to / update existing arguments

        :param new_arguments: (dict)
        """
        if new_arguments:
734
            assert isinstance(new_arguments, dict), 'Arguments not in dict !!!'
735 736 737
            self._arguments.update(new_arguments)
            self.update_parser()

738
    def _parse_required_arguments(self, required, parsed_args):
739
        if not (self.check_required and required):
740 741 742 743 744 745
            return True
        if isinstance(required, tuple):
            for item in required:
                if not self._parse_required_arguments(item, parsed_args):
                    return False
            return True
Stavros Sachtouris's avatar
Stavros Sachtouris committed
746
        elif isinstance(required, list):
747 748 749 750 751 752
            for item in required:
                if self._parse_required_arguments(item, parsed_args):
                    return True
            return False
        return required in parsed_args

Stavros Sachtouris's avatar
Stavros Sachtouris committed
753
    def parse(self, new_args=None):
754
        """Parse user input"""
755
        try:
756 757
            pkargs = (new_args,) if new_args else ()
            self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
758 759 760
            parsed_args = [
                k for k, v in vars(self._parsed).items() if v not in (None, )]
            if not self._parse_required_arguments(self.required, parsed_args):
761
                self.print_help()
762
                raise CLISyntaxError('Missing required arguments')
763 764
        except SystemExit:
            raiseCLIError(CLISyntaxError('Argument Syntax Error'))
765 766 767 768 769 770
        for name, arg in self.arguments.items():
            arg.value = getattr(self._parsed, name, arg.default)
        self._unparsed = []
        for term in unparsed:
            self._unparsed += split_input(' \'%s\' ' % term)
        self._parser_modified = False