Source code for clik.command

# -*- coding: utf-8 -*-
"""
All the recursive, argument-parsin', context-managin' goodness.

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

import re
import sys


#: Globally-unique value used by commands to indicate they want clik to send
#: the exception (if one occurred) in addition to the child exit code in
#: response to a ``yield``.
#:
#: :type: :class:`object`
catch = object()

#: Name of the magic variable containing the parsed arguments.
#:
#: :type: :class:`str`
ARGS = 'args'

#: Name of the argument that contains the stack of commands to be run.
#:
#: :type: :class:`str`
STACK = '_clik_stack'

#: If a parent has more than this number of subcommands, the help output will
#: ``{command}`` instead of the full list of subcommands.
#:
#: :type: :class:`int`
SHOW_SUBCOMMANDS = 3


[docs]class BareAlreadyRegisteredError(Exception): """Raised when a bare command has already been registered.""" def __init__(self, command): """ Initialize the exception. :param command: Command the user is trying to register. :type command: :class:`Command` """ fmt = 'Bare command already registered for command "%s"' super(BareAlreadyRegisteredError, self).__init__(fmt % command._name) #: Command caller was trying to register as the bare command. #: #: :type: :class:`Command` self.command = command
[docs]class Command(object): """The invisible backend driving most of what the user interacts with.""" @staticmethod def _split_docstring(x=None): parts = list(filter(None, re.split(r'\n\s*\n', x.__doc__ or '', 1))) return parts + [None] * (2 - len(parts))
[docs] def __init__(self, ctx, fn, name=None, alias=None, aliases=None): """ Initialize the command object. :param ctx: Context object. :type ctx: :class:`clik.context.Context` :param fn: Generator function -- the actual command. :param str name: Name of the command or ``None``. If ``None``, name will be ``fn.__name__``. :param str alias: Command alias or ``None``. If ``None``, the command has no aliases. If this and ``aliases`` are both supplied, ``alias`` will be prepended to the ``aliases`` list. :param aliases: List of additional aliases for the command or ``None``. :type aliases: :class:`list` or ``None`` """ #: Tuple of aliases for this command. #: #: :type: :class:`tuple` of :class:`str` self._aliases = () #: :class:`Command` instance for the bare command or ``None`` if bare #: command is not set. #: #: :type: :class:`Command` or ``None`` self._bare = None #: List of child commands. #: #: :type: :class:`list` of :class:`Command` instances self._children = [] #: Context object for this command. This context is shared between all #: command instances associated with a :class:`clik.app.App`. #: #: :type: :class:`clik.context.Context` self._ctx = ctx #: Generator function -- the actual command. #: #: :type: generator function self._fn = fn #: In-progress generator for :attr:`_fn`. This is the object that we #: call :meth:`generator.send` and :meth:`generator.next` on. #: #: :type: :class:`generator` self._generator = None #: Canonical name of the command. #: #: :type: :class:`str` self._name = 'Unnamed' #: Parent command. For the root :class:`clik.app.App` instance, this #: is ``None``. Set in :meth:`_configure_parser`. #: #: :type: :class:`Command` or ``None`` self._parent = None #: Parser for this command. Set in :meth:`_configure_parser`. #: #: :type: :class:`clik.argparse.ArgumentParser` self._parser = None if name is None: self._name = fn.__name__ else: self._name = name if aliases is None: aliases = [] else: aliases = list(aliases) if alias is not None: aliases.insert(0, alias) self._aliases = tuple(aliases) self.__doc__ = fn.__doc__
def bare(self, fn): """ Register a bare command. :param fn: Generator function -- the bare command. :raise: :exc:`BareAlreadyRegisteredError` if a bare command has already been registered. """ if self._bare is not None: raise BareAlreadyRegisteredError(self) self._bare = Command(self._ctx, fn) return self._bare
[docs] def __call__(self, fn=None, name=None, alias=None, aliases=None): """ Add subcommands to this command. Basic use:: @myapp def mysubcommand(): yield Customizing the subcommand:: @myapp(name='subcommand', alias='sub', aliases=['s']) def mysubcommand(): yield :param fn: Generator function or ``None``. If ``fn`` is supplied, all other arguments are ignored. :param str name: Name of the command or ``None``. :param str alias: Command alias. See :meth:`__init__` for information on how aliases are handled. :param aliases: List of additional aliases for the command or ``None``. :type aliases: :class:`list` or ``None`` """ def decorate(fn): self._children.append(Command(self._ctx, fn, name, alias, aliases)) return self._children[-1] if fn is None: return decorate return decorate(fn)
[docs] def _configure_parser(self, parser, parent=None, stack=None): self._parser = parser parser._clik_command = self if stack is None: stack = [] if parent is not None: self._parent = parent self._generator = self._fn() with self._ctx(parser=parser): ec = next(self._generator) if ec: return ec stack = stack + [self] if self._bare: self._bare._generator = self._bare._fn() with parser._clik_bare_arguments(): with self._ctx(parser=parser): ec = next(self._bare._generator) if ec: return ec parser.set_defaults(**{STACK: stack + [self._bare]}) if self._children: if len(self._children) > SHOW_SUBCOMMANDS: metavar = '{command}' else: names = ','.join([c._name for c in self._children]) metavar = '{%s}' % names subparsers = parser.add_subparsers( dest='command', metavar=metavar, title='subcommands', ) if not self._bare: subparsers.required = True for child in self._children: description, epilog = self._split_docstring(child._fn) subparser = subparsers.add_parser( child._name, aliases=child._aliases, description=description, epilog=epilog, help=description, ) ec = child._configure_parser(subparser, self, stack) if ec: return ec if not self._bare and not self._children: parser.set_defaults(**{STACK: stack})
[docs] def _check_bare_arguments(self): if self._parent is not None: parser = self._parent._parser if parser._clik_bare_dests is not None: args = self._ctx.get(ARGS) for dest in parser._clik_bare_dests: if getattr(args, dest) != parser.get_default(dest): parser.print_usage(sys.stderr) for action in parser._actions: # pragma: no branch if action.dest == dest: break else: # pragma: no cover (unreachable) raise Exception('this code should be unreachable') options = '/'.join(action.option_strings) err = 'unrecognized arguments when calling subcommand' fmt = '%s: error: %s: %s' msg = fmt % (parser.prog, err, options) print(msg, file=sys.stderr) return 1 delattr(args, dest)
[docs] def _run(self, child=False): stack = getattr(self._ctx.get(ARGS), STACK) if not child: for command in stack: ec = command._check_bare_arguments() if ec: return ec command = stack.pop(0) if stack: def run_children(): return stack[0]._run(child=True) with self._ctx(run_children=run_children): try: ec = next(command._generator) except StopIteration: ec = 0 if ec and ec is not catch: return ec if stack: if ec is catch: ec = 0 exception = None try: ec = run_children() except Exception as e: exception = e send = (ec, exception) else: send = ec = run_children() try: ec = command._generator.send(send) except StopIteration: pass else: def run_children(): return 0 with self._ctx(run_children=run_children): try: ec = next(command._generator) except StopIteration: ec = 0 return ec