Source code for clik.argparse

# -*- coding: utf-8 -*-
"""
Most of the hackery that makes clik tick.

:author: Joe Joyce <joe@decafjoe.com>
:copyright: Copyright (c) Joe Joyce and contributors, 2009-2019.
:license: BSD
"""
from __future__ import absolute_import, print_function

import argparse
import contextlib
import sys

from clik.compat import PY2, PY26, PY33


#: Name of the argument that contains whether to allow unknown
#: arguments.
#:
#: :type: :class:`str`
ALLOW_UNKNOWN = '_clik_unknown'


[docs]class ArgumentParserExit(Exception): """Raised instead of allowing :mod:`argparse` to call :func:`sys.exit`.""" def __init__(self, code): """ Initialize the exception. :param int code: Exit code. """ fmt = 'argument parser exited with return code %i' super(ArgumentParserExit, self).__init__(fmt % code) #: Exit code. #: #: :type: :class:`int` self.code = code
[docs]class BareUnsupportedFeatureError(Exception): """Raised when using a feature that is not supported by bare commands.""" def __init__(self, feature): """ Initialize the exception. :param str feature: Name of the feature that is unsupported. """ msg = 'Feature is not supported for bare commands: %s' % feature super(BareUnsupportedFeatureError, self).__init__(msg) #: Feature that is unsupported. #: #: :type: :class:`str` self.feature = feature
class UnknownArgsUnsupportedError(Exception): """Raised when trying to allow unknown args on a command with children.""" def __init__(self): """Initialize the exception.""" msg = 'Unknown args are not allowed for commands with subcommands' super(UnknownArgsUnsupportedError, self).__init__(msg)
[docs]class HelpFormatter(argparse.HelpFormatter): """Format usage with no trailing newline on usage.""" def _format_usage(self, *args, **kwargs): parent = super(HelpFormatter, self) return parent._format_usage(*args, **kwargs)[:-1]
[docs]class ArgumentParser(argparse.ArgumentParser): """:class:`argparse.ArgumentParser` specialized for clik.""" def __init__(self, *args, **kwargs): """Initialize -- same arguments as :class:`argparse.ArgumentParser`.""" self._clik_bare_command_mode = False self._clik_bare_dests = None self._clik_command = None kwargs.setdefault('formatter_class', HelpFormatter) super(ArgumentParser, self).__init__(*args, **kwargs)
[docs] def exit(self, status=0, message=None): """ Override default behavior to avoid interpreter exit. By default, the parser calls :func:`sys.exit`. In certain situations -- namely testing -- we don't actually want to exit the Python interpreter. So instead of exiting, this throws an :exc:`ArgumentParserExit` exception which can be caught by the caller. :param int status: Exit status. :param str message: Optional message. If supplied, will be printed to :data:`sys.stderr` before raising the exception. :raise: :exc:`ArgumentParserExit` """ if message: print(message, end='', file=sys.stderr) raise ArgumentParserExit(status)
[docs] @contextlib.contextmanager def _clik_bare_arguments(self): """ Context manager for bare command mode. When the parser is in bare command mode, it disallows certain features (like positional args and mutually exclusive groups). In addition, the argument destinations are recorded in order to do some post-processing before running the selected command. """ self._clik_bare_dests = [] self._clik_bare_command_mode = True yield self._clik_bare_command_mode = False
[docs] def add_argument(self, *args, **kwargs): """ Override default behavior to disallow posargs in bare commands. :raise: :class:`BareUnsupportedFeatureError` if adding a positional argument to a bare command. """ argument = super(ArgumentParser, self).add_argument(*args, **kwargs) if self._clik_bare_command_mode: if argument.nargs: msg = 'positional arguments (dest: %s)' % argument.dest raise BareUnsupportedFeatureError(msg) self._clik_bare_dests.append(argument.dest) return argument
[docs] def add_mutually_exclusive_group(self, *args, **kwargs): """ Override default behavior to disallow mutex groups in bare commands. :raise: :class:`BareUnsupportedFeatureError` if adding a mutually exclusive group to a bare command. """ if self._clik_bare_command_mode: raise BareUnsupportedFeatureError('mutually exclusive groups') s = super(ArgumentParser, self) return s.add_mutually_exclusive_group(*args, **kwargs)
[docs] def allow_unknown_args(self): """ Allow unknown arguments, putting them in ``clik.unknown_args``. :raise: :exc:`UnknownArgsUnsupportedError` if this parser has subparsers -- unknown arguments are allowed *only* on leaf commands. """ if self._clik_command._children: raise UnknownArgsUnsupportedError self.set_defaults(**{ALLOW_UNKNOWN: True})
def _clik_format_usage(self, formatter): bare_dests = self._clik_bare_dests actions, bare_actions, subparsers = [], [], None for action in self._actions: if isinstance(action, argparse._SubParsersAction): subparsers = action actions.append(action) else: bare_actions.append(action) if bare_dests is None or action.dest not in bare_dests: actions.append(action) prefix = 'usage: ' mutex_groups = self._mutually_exclusive_groups if bare_dests is None or subparsers: formatter.add_usage(self.usage, actions, mutex_groups, prefix) prefix = ' ' if bare_dests is not None: formatter.add_usage(self.usage, bare_actions, mutex_groups, prefix) return formatter
[docs] def format_usage(self): """Override default behavior to use clik's formatter.""" return self._clik_format_usage(self._get_formatter()).format_help()
[docs] def format_help(self): """Override default behavior to support formatting bare commands.""" formatter = self._get_formatter() self._clik_format_usage(formatter) formatter.add_text('\n') formatter.add_text(self.description) for action_group in self._action_groups: formatter.start_section(action_group.title) formatter.add_text(action_group.description) formatter.add_arguments(action_group._group_actions) formatter.end_section() formatter.add_text(self.epilog) return formatter.format_help()
if PY33 or PY26: # pragma: no cover (~copypaste of Python 3.4 implementation) # LINT: Ignore undocumented function violation. Compatibility code is not # formally documented. def __call__(self, parser, namespace, values, option_string=None): # noqa parser_name = values[0] arg_strings = values[1:] if self.dest is not argparse.SUPPRESS: setattr(namespace, self.dest, parser_name) try: parser = self._name_parser_map[parser_name] except KeyError: fmt = 'unknown parser %(parser_name)r (choices: %(choices)s)' args = { 'parser_name': parser_name, 'choices': ', '.join(self._name_parser_map), } raise argparse.ArgumentError(self, fmt % args) subnamespace, arg_strings = parser.parse_known_args(arg_strings, None) for key, value in vars(subnamespace).items(): setattr(namespace, key, value) if arg_strings: vars(namespace).setdefault(argparse._UNRECOGNIZED_ARGS_ATTR, []) attr = getattr(namespace, argparse._UNRECOGNIZED_ARGS_ATTR) attr.extend(arg_strings) argparse._SubParsersAction.__call__ = __call__ if PY2: original_error = ArgumentParser.error # LINT: Ignore undocumented function violation. Compatibility code is not # formally documented. def error(self, message=None): # noqa: D103 if message == 'too few arguments': for action in self._actions: if isinstance(action, argparse._SubParsersAction): name = action.metavar break else: for action in self._positionals._actions: # pragma: no branch if action.required: name = action.dest break else: # pragma: no cover (unreachable) raise Exception('this code should be unreachable') if action.required: self.print_usage(sys.stderr) fmt = '%s: error: the following arguments are required: %s\n' return self.exit(2, fmt % (self.prog, name)) else: return original_error(self, message) ArgumentParser.error = error # https://gist.github.com/sampsyo/471779 # LINT: Ignore undocumented function violation. Compatibility code is not # formally documented. class _AliasedSubParsersPseudoAction(argparse.Action): # noqa: D103 def __init__(self, name, aliases, help): dest = name if aliases: dest += ' (%s)' % ', '.join(aliases) parent = super(_AliasedSubParsersPseudoAction, self) parent.__init__(option_strings=[], dest=dest, help=help) original_add_parser = argparse._SubParsersAction.add_parser # LINT: Ignore undocumented function violation. Compatibility code is not # formally documented. def add_parser(self, name, **kwargs): # noqa: D103 aliases = kwargs.pop('aliases') parser = original_add_parser(self, name, **kwargs) for alias in aliases: self._name_parser_map[alias] = parser help = kwargs.pop('help') self._choices_actions.pop() action = _AliasedSubParsersPseudoAction(name, aliases, help) self._choices_actions.append(action) return parser argparse._SubParsersAction.add_parser = add_parser