# -*- 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 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
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