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