Getting started

Motivation

Suppose we want a simple script that adds or multiplies two numbers. This code should then be

  1. callable inside python (i.e. we create a function)

  2. executable from the command line

So let’s setup the function in a file called 'add_or_multiply.py' like this

In [1]: def do_something(a, b, multiply=False):
   ...:    """
   ...:    Multiply or add one number to the others
   ...: 
   ...:    Parameters
   ...:    ----------
   ...:    a: int
   ...:        Number 1
   ...:    b: list of int
   ...:        A list of numbers to add `a` to
   ...:    multiply: bool
   ...:        If True, the numbers are multiplied, not added
   ...:    """
   ...:    if multiply:
   ...:        result = [n * a for n in b]
   ...:    else:
   ...:        result = [n + a for n in b]
   ...:    print(result)
   ...: 

Now, if you want to make a command line script out of it, the usual methodology is to create an argparse.ArgumentParser instance and parse the arguments like this

In [2]: if __name__ == '__main__':
   ...:     from argparse import ArgumentParser
   ...:     parser = ArgumentParser(
   ...:         description='Multiply or add two numbers')
   ...:     parser.add_argument('a', type=int, help='Number 1')
   ...:     parser.add_argument('b', type=int, nargs='+',
   ...:                         help='A list of numbers to add `a` to')
   ...:     parser.add_argument('-m', '--multiply', action='store_true',
   ...:                         help='Multiply the numbers instead of adding them')
   ...:     args = parser.parse_args('3 2 -m'.split())
   ...:     do_something(**vars(args))
   ...: 

Now, if you parse the arguments, you get

In [3]: parser.print_help()
usage: __main__.py [-h] [-m] a b [b ...]

Multiply or add two numbers

positional arguments:
  a               Number 1
  b               A list of numbers to add `a` to

optional arguments:
  -h, --help      show this help message and exit
  -m, --multiply  Multiply the numbers instead of adding them

However, you could skip the entire lines above, if you just use the funcargparse.FuncArgParser

In [4]: from funcargparse import FuncArgParser

In [5]: parser = FuncArgParser()

In [6]: parser.setup_args(do_something)
Out[6]: <function __main__.do_something(a, b, multiply=False)>

In [7]: parser.update_short(multiply='m')

In [8]: actions = parser.create_arguments()

In [9]: parser.print_help()
usage: __main__.py [-h] [-m] int int [int ...]

Multiply or add one number to the others

positional arguments:
  int             Number 1
  int             A list of numbers to add `a` to

optional arguments:
  -h, --help      show this help message and exit
  -m, --multiply  If True, the numbers are multiplied, not added

or you use the parser right in the beginning as a decorator

In [10]: @parser.update_shortf(multiply='m')
   ....: @parser.setup_args
   ....: def do_something(a, b, multiply=False):
   ....:    """
   ....:    Multiply or add one number to the others
   ....: 
   ....:    Parameters
   ....:    ----------
   ....:    a: int
   ....:        Number 1
   ....:    b: list of int
   ....:        A list of numbers to add `a` to
   ....:    multiply: bool
   ....:        If True, the numbers are multiplied, not added
   ....:    """
   ....:    if multiply:
   ....:        result = [n * a for n in b]
   ....:    else:
   ....:        result = [n + a for n in b]
   ....:    print(result)
   ....: 

In [11]: actions = parser.create_arguments()

In [12]: parser.print_help()
usage: __main__.py [-h] [-m] int int [int ...]

Multiply or add one number to the others

positional arguments:
  int             Number 1
  int             A list of numbers to add `a` to

optional arguments:
  -h, --help      show this help message and exit
  -m, --multiply  If True, the numbers are multiplied, not added

The FuncArgParser interpretes the docstring (see Interpretation guidelines for docstrings) and sets up the arguments.

Your '__main__' part could then simply look like

In [13]: if __name__ == '__main__':
   ....:     parser.parse_to_func()
   ....: 

Usage

Generally the usage is

  1. create an instance of the FuncArgParser class

  2. setup the arguments using the setup_args() function

  3. modify the arguments (optional) either

    1. in the FuncArgParser.unfinished_arguments dictionary

    2. using the update_arg(), update_short(), update_long() or append2help() methods

    3. using the equivalent decorator methods update_argf(), update_shortf(), update_longf() or append2helpf()

  4. create the arguments using the create_arguments() method

Subparsers

You can also use subparsers for controlling you program (see the argparse.ArgumentParser.add_subparsers() method). They can either be implemented the classical way via

In [14]: subparsers = parser.add_subparsers()

In [15]: subparser = subparsers.add_parser('test')

And then as with the parent parser you can use function docstrings.

In [16]: @subparser.setup_args
   ....: def my_other_func(b=1):
   ....:     """
   ....:     Subparser summary
   ....: 
   ....:     Parameters
   ....:     ----------
   ....:     b: int
   ....:         Anything"""
   ....:     print(b * 500)
   ....: 

In [17]: subparser.create_arguments()
Out[17]: [_StoreAction(option_strings=['-b'], dest='b', nargs=None, const=None, default=1, type=<class 'int'>, choices=None, help='Anything', metavar='int')]

In [18]: parser.print_help()
usage: __main__.py [-h] {test} ...

positional arguments:
  {test}

optional arguments:
  -h, --help  show this help message and exit

On the other hand, you can use the setup_subparser() method to directly create the subparser

In [19]: parser.add_subparsers()
Out[19]: _SubParsersAction(option_strings=[], dest='==SUPPRESS==', nargs='A...', const=None, default=None, type=None, choices={}, help=None, metavar=None)

In [20]: @parser.setup_subparser
   ....: def my_other_func(b=1):
   ....:     """
   ....:     Subparser summary
   ....: 
   ....:     Parameters
   ....:     ----------
   ....:     b: int
   ....:         Anything"""
   ....:     print(b * 500)
   ....: 

In [21]: parser.create_arguments(subparsers=True)
Out[21]: []

In [22]: parser.print_help()
usage: __main__.py [-h] {my-other-func} ...

positional arguments:
  {my-other-func}
    my-other-func  Subparser summary

optional arguments:
  -h, --help       show this help message and exit

which now created the my-other-func sub command.

Chaining subparsers

Separate from the usage of the function docstring, we implemented the possibilty to chain subparsers. This changes the handling of subparsers compared to the default behaviour (which is inherited from the argparse.ArgumentParser). The difference can be shown in the following example

In [23]: from argparse import ArgumentParser

In [24]: argparser = ArgumentParser()

In [25]: funcargparser = FuncArgParser()

In [26]: sps_argparse = argparser.add_subparsers()

In [27]: sps_funcargparse = funcargparser.add_subparsers(chain=True)

In [28]: sps_argparse.add_parser('dummy').add_argument('-a')
Out[28]: _StoreAction(option_strings=['-a'], dest='a', nargs=None, const=None, default=None, type=None, choices=None, help=None, metavar=None)

In [29]: sps_funcargparse.add_parser('dummy').add_argument('-a')
Out[29]: _StoreAction(option_strings=['-a'], dest='a', nargs=None, const=None, default=None, type=None, choices=None, help=None, metavar=None)

In [30]: ns_default = argparser.parse_args('dummy -a 3'.split())

In [31]: ns_chained = funcargparser.parse_args('dummy -a 3'.split())

In [32]: print(ns_default, ns_chained)
Namespace(a='3') Namespace(dummy=Namespace(a='3'))

So while the default behaviour is, to put the arguments in the main namespace like

In [33]: ns_default.a
Out[33]: '3'

the chained subparser procedure puts the commands for the 'dummy' command into an extra namespace like

In [34]: ns_chained.dummy.a
Out[34]: '3'

This has the advantages that we don’t mix up subparsers if we chain them. So here is an example demonstrating the power of it

In [35]: sps_argparse.add_parser('dummy2').add_argument('-a')
Out[35]: _StoreAction(option_strings=['-a'], dest='a', nargs=None, const=None, default=None, type=None, choices=None, help=None, metavar=None)

In [36]: sps_funcargparse.add_parser('dummy2').add_argument('-a')
Out[36]: _StoreAction(option_strings=['-a'], dest='a', nargs=None, const=None, default=None, type=None, choices=None, help=None, metavar=None)

# with allowing chained subcommands, we get
In [37]: ns_chained = funcargparser.parse_args('dummy -a 3 dummy2 -a 4'.split())

In [38]: print(ns_chained.dummy.a, ns_chained.dummy2.a)
3 4

# on the other side, the default ArgumentParser raises an error because
# chaining is not allowed
In [39]: ns_default = argparser.parse_args('dummy -a 3 dummy2 -a 4'.split())
An exception has occurred, use %tb to see the full traceback.

SystemExit: 2

Furthermore, you can use the parse_chained() and the parse_known_chained() methods to parse directly to the subparsers.

In [40]: parser = FuncArgParser()

In [41]: sps = parser.add_subparsers(chain=True)

In [42]: @parser.setup_subparser
   ....: def subcommand_1():
   ....:     print('Calling subcommand 1')
   ....:     return 1
   ....: 

In [43]: @parser.setup_subparser
   ....: def subcommand_2():
   ....:    print('Calling subcommand 2')
   ....:    return 2
   ....: 

In [44]: parser.create_arguments(True)
Out[44]: []

In [45]: parser.parse_chained('subcommand-1 subcommand-2'.split())
Calling subcommand 1
Calling subcommand 2
Out[45]: Namespace(subcommand_1=1, subcommand_2=2)

Warning

If you reuse an already existing command in the subcommand of another subcommand, the latter one get’s prefered. See this example

In [46]: sp = sps.add_parser('subcommand-3')

In [47]: sps1 = sp.add_subparsers(chain=True)

# create the same subparser subcommand-1 but as a subcommand of the
# subcommand-3 subparser
In [48]: @sp.setup_subparser
   ....: def subcommand_1():
   ....:     print('Calling modified subcommand 1')
   ....:     return 3.1
   ....: 

In [49]: sp.create_arguments(True)
Out[49]: []

# subcommand-1 get's called
In [50]: parser.parse_chained('subcommand-1 subcommand-3'.split())
Calling subcommand 1
Out[50]: Namespace(subcommand_3=None, subcommand_1=1)

# subcommand-3.subcommand-1 get's called
In [51]: parser.parse_chained('subcommand-3 subcommand-1'.split())
Calling modified subcommand 1
Out[51]: Namespace(subcommand_3=Namespace(subcommand_1=3.1))