"""
Addition to optparse to automatically parse and store the arguments.

This module provides a new class OptionParserWithArgs() that allows specifying
the contents of the arguments (always non-optional) and that places the results
on the resulting options container object rather than return the arguments.


10 Second User Guide
--------------------

Use it like this::

    import optparse_wargs as optparse
    ...

    parser = optparse.OptionParserWithArgs(...)

    ...
    parser.add_argument('dbname', 'The database name to work on.')
    parser.add_argument('dumpfile', 'The dump file to work on.')

    opts = parser.parse_args()

    # Use opts.dbname, opts.dumpfile, ...
    

Notes
-----

* The ``parse_args()`` function does not return the arguments anymore, they are
  stored on the options object.

* Collisions between option attribute names and argument names are detected.
  Specifying an incorrect number of arguments will fail automatically.

* The generated usage help now includes the documentation for the arguments.

* When you add an argument, you can specify how many of the arguments to
  consume.  Using -1 will store all the remaining arguments in the destination.

To Do
-----

* We need to submit this to Optik.
* Write tests.
* Actions would be convenient for arguments as well.


"""
__author__ = 'Martin Blais <blais@furius.ca>'

import optparse
import textwrap
from gettext import gettext as _


class OptionParserWithArgs(optparse.OptionParser):
    """
    OptionParser that allows one to include descriptions of the arguments to be
    parsed and returned. 
    """
    def __init__(self, *args, **kwds):
        optparse.OptionParser.__init__(self, *args, **kwds)

        self.argument_list = []
        """Description of the arguments to be parsed."""

    def add_argument(self, dest, description=None, nargs=1):
        """
        'dest': the attribute name where we will store the argument value.
        'description': a description of the argument.  Maybe be None.
        'nargs': the number of arguments to consume.  If -1, all the remainder
                 of the arguments are consumed.
        """
        assert nargs >= 1 or nargs == -1
        if self.has_option(dest):
            self.error("Collision between options and arguments.")
            
        self.argument_list.append( (dest, description, nargs) )

    def parse_args(self, *args, **kwds):
        """
        Like the base class, except that the arguments are extracted into the
        opts object and the number of argument is checked.
        """
        opts, args = optparse.OptionParser.parse_args(self, *args, **kwds)

        for dest, description, nargs in self.argument_list:
            if len(args) < nargs:
                self.error("Not enough arguments.")
            if nargs == -1:
                value = args
                args = []
            elif nargs == 1:
                value = args[0]
                args = args[1:]
            else:
                value = args[0:nargs]
                args = args[nargs:]
                
            setattr(opts, dest, value)

        if args:
            self.error("Extraneous arguments: %s" % ', '.join(args))

        return opts

    def format_help(self, formatter=None):
        if formatter is None:
            formatter = self.formatter
        result = []
        if self.usage:
            result.append(self.get_usage() + "\n")
        if self.description:
            result.append(self.format_description(formatter) + "\n")
        opthelp = self.format_option_help(formatter)
        arghelp = self.format_argument_help(formatter)
        result.append(arghelp)
        if opthelp and arghelp:
            result.append("\n")
        result.append(opthelp)
        return "".join(result)

    def format_argument_help(self, formatter=None):
        if formatter is None:
            formatter = self.formatter
        result = []
        result.append(formatter.format_heading(_("arguments")))
        formatter.indent()
        if self.argument_list:
            result.append(
                optparse.OptionContainer.format_argument_help(self, formatter))
            result.append("\n")
        formatter.dedent()
        # Drop the last "\n", or the header if no argument:
        return "".join(result[:-1])


def OptionContainer__format_argument_help(self, formatter):
    if not self.argument_list:
        return ""
    result = []
    for argdesc in self.argument_list:
        result.append(formatter.format_argument(argdesc))
    return "".join(result)

def HelpFormatter__format_argument(self, argdesc):
    """
    Format an argument description.
    """
    dest, help_text, nargs = argdesc

    result = []

    if nargs == 1:
        names = '<%s>' % dest
    elif nargs == -1:
        names = '<%s> ...' % dest
    else:
        names = ', '.join(map(lambda x, n: '<%s%d>' % (dest, n), xrange(nargs)))

    opt_width = self.help_position - self.current_indent - 2
    if len(names) > opt_width:
        names = "%*s%s\n" % (self.current_indent, "", names)
        indent_first = self.help_position
    else:                       # start help on same line as names
        names = "%*s%-*s  " % (self.current_indent, "", opt_width, names)
        indent_first = 0
    result.append(names)

    if help_text:
        help_lines = textwrap.wrap(help_text, self.help_width)
        result.append("%*s%s\n" % (indent_first, "", help_lines[0]))
        result.extend(["%*s%s\n" % (self.help_position, "", line)
                       for line in help_lines[1:]])
    elif names[-1] != "\n":
        result.append("\n")
    return "".join(result)
    

# FIXME: Remove this monkey patching and merge on HelpFormatter in optparse.py.
optparse.HelpFormatter.format_argument = \
    HelpFormatter__format_argument
optparse.OptionContainer.format_argument_help = \
    OptionContainer__format_argument_help


