Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python’s argh library: preserve docstring formatting in help message

While searching for faster ways to parse command-line arguments in my scripts I came across the argh library.

I really like the features of argh but I’ve encountered one drawback that stops me from using it, and this has to do with the default help message that gets displayed if I’m invoking the —help option: per default the function’s docstring is displayed on top of the arguments list. This is great, however the initial formatting is lost. See, for example, the following example script

import argh

def func(foo=1, bar=True):
    """Sample function.

        Parameters:
            foo: float
                An example argument.
            bar: bool
                Another argument.
    """
    print foo, bar

argh.dispatch_command(func, argv=['-h'])

which would result in the following output

usage: script.py [-h] [-f FOO] [-b]

Sample function. Parameters: foo: float An example argument. bar: bool Another
argument.

optional arguments:
  -h, --help         show this help message and exit
  -f FOO, --foo FOO
  -b, --bar

Is there an (easy) way to get an output like the following?

usage: script.py [-h] [-f FOO] [-b]

Sample function.

    Parameters:
        foo: float
            An example argument.
        bar: bool
            Another argument.

optional arguments:
  -h, --help         show this help message and exit
  -f FOO, --foo FOO
  -b, --bar

I'd prefer to not use annotations to define the argument help messages since that would require me to alter both the function's docstring AND the help text each time there is something to change.

like image 286
nilfisque Avatar asked May 09 '14 14:05

nilfisque


2 Answers

I'm not familiar with argh, but apparently it is a wrapper to argparse. My guess is that it is taking your function __doc__, and making it the description of a parser, e.g.

parser = argparse.ArgumentParser(description=func.__doc__)

https://docs.python.org/2.7/library/argparse.html#argparse.RawDescriptionHelpFormatter

argparse has a RawDescriptionHelpFormatter that displays the description as is.

parser = argparse.ArgumentParser(description=func.__doc__,
    formatter_class=argparse.RawDescriptionHelpFormatter)

So the question is, is there a way of getting argh to use this formatter?

This argparse script produces the help that you want:

import argparse

def func(foo=1, bar=True):
    """Sample function.

        Parameters:
            foo: float
                An example argument.
            bar: bool
                Another argument.
    """
    print foo, bar

parser = argparse.ArgumentParser(prog='script.py',
    description=func.__doc__,
    formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-f', '--foo', type=float)
parser.add_argument('-b', '--bar', action='store_false')
parser.print_help()

In argh/dispatching.py

def dispatch_command(function, *args, **kwargs):
    ...
    parser = argparse.ArgumentParser(formatter_class=PARSER_FORMATTER)
    set_default_command(parser, function)
    dispatch(parser, *args, **kwargs)

So you could either set:

PARSER_FORMATTER = argparse.RawDescriptionHelpFormatter

or write your own function:

def raw_dispatch_command(function, *args, **kwargs):
    ...
    parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter)
    set_default_command(parser, function)
    dispatch(parser, *args, **kwargs)
like image 144
hpaulj Avatar answered Nov 20 '22 05:11

hpaulj


With the help of @hpaulj I finally managed to obtain the desired behaviour. To facilitate this I defined a custom decorator similar to argh.arg, with the goal to not have to write @argh.arg(‘—param’, help=“%(default)s”) for each parameter separately, but instead to only use one @arg_custom() decorator on my function:

def arg_custom():
    from argh.constants import ATTR_ARGS
    from argh.assembling import  _get_args_from_signature, _fix_compat_issue29

    def wrapper(func):
        declared_args = getattr(func, ATTR_ARGS, [])
        for a in list(_get_args_from_signature(func)):
             declared_args.insert(0, dict(option_strings=a['option_strings'], help="(default: %(default)s)"))
        setattr(func, ATTR_ARGS, declared_args)
        _fix_compat_issue29(func)
        return func
    return wrapper

The crucial point here is that a for loop takes care that all arguments get a corresponding help=“%(default)s” option. Together with changing the corresponding lines in argh/constants.py

class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
        pass
PARSER_FORMATTER = CustomFormatter

we can now conveniently use

@arg_custom()
def func(foo=1, bar=True):
    """Sample function.

        Parameters:
            foo: float
                An example argument.
            bar: bool
                Another argument.
    """
    print foo, bar

argh.dispatch_command(func)

yielding finally

usage: script.py [-h] [-f FOO] [-b]

Sample function.

        Parameters:
            foo: float
                An example argument.
            bar: bool
                Another argument.


optional arguments:
  -h, --help         show this help message and exit
  -f FOO, --foo FOO  (default: 1)
  -b, --bar          (default: True) 

when executing the script with the -h option.

like image 3
nilfisque Avatar answered Nov 20 '22 06:11

nilfisque