Source code for clams

"""
Clams
=====

Create simple, nested, command-line interfaces.


Example
-------

A simple example with ``hello`` and ``goodbye`` subcommands.  This can be found
at `/demo/salutation.py </demo/salutation.py>`_.

.. testcode::

   from clams import arg, Command

   salutation = Command('salutation')

   @salutation.register('hello')
   @arg('name', nargs='?')  # <== same interface as argparse's `add_argument`
   def handler(name):
       print 'Hello %s' % name or 'Nick'

   @salutation.register('goodbye')
   @arg('name', nargs='?')
   def handler(name):
       print 'Goodbye %s' % name or 'Nick'

   if __name__ == '__main__':
       salutation.init()
       salutation.parse_args()

Usage:

.. code-block:: console

   $ cd demo

   $ ./salutation.py hello
   Hello Nick

   $ ./salutation.py hello Jason
   Hello Jason

   $ ./salutation.py goodbye "my friend."
   Goodbye my friend.


For more in-depth examples, see the `/demo </demo>`_ directory.

.. doctest::
   :hide:

   >>> salutation.init()
   >>> salutation.parse_args(['hello', 'Bob'])
   Hello Bob
   >>> salutation.parse_args(['goodbye', 'Alice'])
   Goodbye Alice

"""

import argparse
import textwrap


[docs]def arg(*args, **kwargs): """Annotate a function by adding the args/kwargs to the meta-data. This appends an Argparse "argument" to the function's ``ARGPARSE_ARGS_LIST`` attribute, creating ``ARGPARSE_ARGS_LIST`` if it does not already exist. Aside from that, it returns the decorated function unmodified, and unwrapped. The "arguments" are simply ``(args, kwargs)`` tuples which will be passed to the Argparse parser created from the function as ``parser.add_argument(*args, **kwargs)``. `argparse.ArgumentParser.add_argument <https://docs.python.org/2/library/argparse.html#the-add-argument-method>`_ should be consulted for up-to-date documentation on the accepted arguments. For convenience, a list has been included here. Args ---- name/flags : str or list Either a name or a list of (positional) option strings, e.g. ('foo') or ('-f', '--foo'). action : str The basic type of action to be taken when this argument is encountered at the command line. nargs : str The number of command-line arguments that should be consumed. const A constant value required by some action and nargs selections. default The value produced if the argument is absent from the command line. type : type The type to which the command-line argument should be converted. choices A container of the allowable values for the argument. required : bool Whether or not the command-line option may be omitted (optionals only). help : str A brief description of what the argument does. metavar : str A name for the argument in usage messages. dest : str The name of the attribute to be added to the object returned by parse_args(). Example ------- .. testsetup:: mycommand = Command(name='mycommand') .. testcode:: @command(name='echo') @arg('-n', '--num', type=int, default=42) @arg('-s', '--some-switch', action='store_false') @arg('foo') def echo(foo, num, some_switch): print foo, num .. doctest:: >>> echo_subcommand = mycommand.add_subcommand(echo) >>> mycommand.init() >>> mycommand.parse_args(['echo', 'hi', '-n', '42']) hi 42 See also -------- `argparse.ArgumentParser.add_argument <https://docs.python.org/2/library/argparse.html#the-add-argument-method>`_ """ def annotate(func): # Get the list of argparse args already added to func (if any). argparse_args_list = getattr(func, 'ARGPARSE_ARGS_LIST', []) # Since we're only annotating (not wrapping) the function, appending # the argument to the list would result in the decorators being applied # in reverse order. To prevent that, we simply add to the beginning. argparse_args_list.insert(0, (args, kwargs)) setattr(func, 'ARGPARSE_ARGS_LIST', argparse_args_list) return func return annotate
def _parse_doc(doc=''): """Parse a docstring into title and description. Args ---- doc : str A docstring, optionally with a title line, separated from a description line by at least one blank line. Returns ------- title : str The first line of the docstring. description : str The rest of a docstring. """ title, description = '', '' if doc: sp = doc.split('\n', 1) title = sp[0].strip() if len(sp) > 1: description = textwrap.dedent(sp[1]).strip() return (title, description)
[docs]def command(name): """Create a command, using the wrapped function as the handler. Args ---- name : str Name given to the created Command instance. Returns ------- Command A new instance of Command, with handler set to the wrapped function. """ # TODO(nick): It would be nice if this didn't transform the handler. That # way, handlers could be used and tested independently of this system. # Unfortunately that's one of the better properties of the previous # system that wasn't preserved in this rewrite. def wrapper(func): title, description = _parse_doc(func.__doc__) command = Command(name=name, title=title, description=description) command.add_handler(func) argparse_args_list = getattr(func, 'ARGPARSE_ARGS_LIST', []) for args, kwargs in argparse_args_list: command.add_argument_tuple((args, kwargs)) return command return wrapper
[docs]def register(command): """Register a command with a parent command. The ``register`` decorator decorates a Command instance (not a function). It is intended to be used with the ``command`` decorator (which decorates a function and returns a Command instance). Args ---- comand : Command The parent command. Example ------- .. testcode:: mygit = Command(name='status') @register(mygit) @command('status') def status(): print 'Nothing to commit.' .. doctest:: :hide: >>> mygit.init() >>> mygit.parse_args(['status']) Nothing to commit. """ def wrapper(subcommand): command.add_subcommand(subcommand) return subcommand return wrapper
[docs]def register_command(parent_command, name): """Create and register a command with a parent command. Args ---- parent_comand : Command The parent command. name : str Name given to the created Command instance. Example ------- .. testcode:: mygit = Command(name='status') @register_command(mygit, 'status') def status(): print 'Nothing to commit.' .. doctest:: :hide: >>> mygit.init() >>> mygit.parse_args(['status']) Nothing to commit. """ def wrapper(func): c = command(name)(func) parent_command.add_subcommand(c) return wrapper
[docs]class Command(object): def __init__(self, name, title='', description=''): self.name = name self.title = title self.description = description self.arguments = [] self.subcommands = [] self.handler = None self.parser = None self.initialized = False # has the _init method been called?
[docs] def add_argument_tuple(self, arg_tuple): """Add a new argument to this Command. Args ---- arg_tuple : tuple A tuple of ``(*args, **kwargs)`` that will be passed to ``argparse.ArgumentParser.add_argument``. """ self.arguments.append(arg_tuple)
[docs] def add_subcommand(self, command): """Add a new subcommand to this Command. Args ---- command : Command The Command instance to add. """ self.subcommands.append(command) return command
[docs] def add_handler(self, handler): """Add a handler to be called with the parsed argument namespace. Args ---- handler : function A function that accepts the arguments defined for this command. """ self.handler = handler
def _register_handler(self, subparser, handler): """Add a handler as a default ``_func`` attribute to a subparser. Args ---- subparser : argparse.ArgumentParser The subparser to add the handler to. handler : function The function to add to the subparser, which will be called with the namespace returned by the subparser as kwargs. Returns ------- None """ subparser.set_defaults(_func=handler) def _get_handler(self, namespace, remove_handler=False): """Get a handler (if present) from a namespace. Returns ------- function or None The handler defined in the namespace. """ if hasattr(namespace, '_func'): _func = namespace._func if remove_handler: del namespace._func return _func def _attach_arguments(self): """Add the registered arguments to the parser.""" for arg in self.arguments: self.parser.add_argument(*arg[0], **arg[1]) def _attach_subcommands(self): """Create a subparser and add the registered commands to it. This will also call ``_init`` on each subcommand (in turn invoking its ``_attach_subcommands`` method). """ if self.subcommands: self.subparsers = self.parser.add_subparsers() for subcommand in self.subcommands: subparser = self.subparsers.add_parser(subcommand.name, help=subcommand.title) if subcommand.handler: self._register_handler(subparser, subcommand.handler) subcommand._init(subparser) def _init(self, parser): """Initialize/Build the ``argparse.ArgumentParser`` and subparsers. This internal version of ``init`` is used to ensure that all subcommands have a properly initialized parser. Args ---- parser : argparse.ArgumentParser The parser for this command. """ assert isinstance(parser, argparse.ArgumentParser) self._init_parser(parser) self._attach_arguments() self._attach_subcommands() self.initialized = True def _init_parser(self, parser): self.parser = parser self.parser.title = self.title self.parser.description = self.description self.parser.formatter_class = argparse.RawDescriptionHelpFormatter
[docs] def init(self): """Initialize/Build the ``argparse.ArgumentParser`` and subparsers. This must be done before calling the ``parse_args`` method. """ parser = argparse.ArgumentParser() self._init(parser)
[docs] def parse_args(self, args=None, namespace=None): """Parse the command-line arguments and call the associated handler. The signature is the same as `argparse.ArgumentParser.parse_args <https://docs.python.org/2/library/argparse.html#argparse.ArgumentParser.parse_args>`_. Args ---- args : list A list of argument strings. If ``None`` the list is taken from ``sys.argv``. namespace : argparse.Namespace A Namespace instance. Defaults to a new empty Namespace. Returns ------- The return value of the handler called with the populated Namespace as kwargs. """ assert self.initialized, '`init` must be called before `parse_args`.' namespace = self.parser.parse_args(args, namespace) handler = self._get_handler(namespace, remove_handler=True) if handler: return handler(**vars(namespace)) # Decorators # ----------
[docs] def register_command(self, name): """Decorator to create and register a command from a function. Args ---- name : str The name given to the registered command. Example ------- .. testcode:: mygit = Command(name='mygit') @mygit.register_command(name='status') def status(): print 'Nothing to commit.' .. doctest:: :hide: >>> mygit.init() >>> mygit.parse_args(['status']) Nothing to commit. """ return register_command(self, name)
[docs] def register(self, name=None): """Decorator to (create and) register a command from a function. Args ---- name : Optional[str] If present, create a command and register it (see ``register_command``). Example ------- .. testcode:: mygit = Command(name='status') @mygit.register(name='status') def status(): print 'Nothing to commit.' @mygit.register() @command(name='log') def log(): print 'Show logs.' .. doctest:: :hide: >>> mygit.init() >>> mygit.parse_args(['status']) Nothing to commit. >>> mygit.parse_args(['log']) Show logs. """ if name is None: return register(self) else: return self.register_command(name)