Basic Usage

By itself, ArgTyper will only create parsers with very basic additional information on arguments. To enhance the generated output and extend functionality, ArgTyper provides a set of decorators. The following examples provide an overview of some of the features of ArgTyper.

Command

The argtyper.Command decorator can be used to pass additional arguments to the created ArgumentParser instance:

import argtyper


@argtyper.Command(
    description="Having a better description can improve your CLIs",
    epilog="And some more text at the end",
)
def hello(name: str, amount: int = 2):
    print("\n".join([f"Hello {name.upper()}"] * amount))


at = argtyper.ArgTyper(hello)
at()

This will produce the following output for the parser.

$ python command.py -h
usage: command.py [-h] [--amount AMOUNT] NAME

Having a better description can improve your CLIs

positional arguments:
  NAME             provide argument of type STR

optional arguments:
  -h, --help       show this help message and exit
  --amount AMOUNT  provide argument of type INT

And some more text at the end

It takes the same arguments that you can pass to argparse.ArgumentParser. You can also pass help as argument, which will be used if the command is used in a subparser.

Additionally, Command also gives you the ability to exclude arguments from the generated parser, set default values or hardcode the values for certain argument names or types. A description of the corresponding arguments can be found in the class description.

Argument

The argtyper.Argument decorator can be used to pass additional arguments to parser.add_argument() It takes the same arguments that you can pass to argparse.ArgumentParser.add_argument. The main difference is that the first argument here is the name of the function argument to annotate.

import argtyper


@argtyper.Argument(
    "amount", "repetitions", help="How often should we say hello?", metavar="reps"
)
@argtyper.Argument("name", "--name", "--n", help="Give me your name", default="Yoda")
def hello(name: str, amount: int = 2):
    print("\n".join([f"Hello {name.upper()}"] * amount))


at = argtyper.ArgTyper(hello, version="This is %(prog)s version 1.3.3.7")
at()

This will produce the following output for the parser.

$ python argument.py -h
usage: argument.py [-h] [--name NAME] [--version] REPS

positional arguments:
  REPS                  How often should we say hello?

optional arguments:
  -h, --help            show this help message and exit
  --name NAME, --n NAME
                        Give me your name
  --version, -v         show program's version number and exit

In this example we changed settings for the arguments amount and name. This way we added more information to the help output. But additionally, we are also able to change the underlying parser.

In this case, we changed the optional argument amount to a required argument with the new name reps. The important thing here is the argument repetitions, which does not include the optional argument prefix -. Therefore it will be interpreted as required.

Conversely, we changed the argument name to an optional argument by using names which are prefixed with a -. Additionally we set a default value (Yoda) to be passed to name, in case the argument is not passed on the command line.

SubCommand

The argtyper.SubCommand decorator can be used to create additional subparsers. This is merely a helper function which does not have any direct counterpart in argparse. It does, however, reuse information attached with argtyper.Command to subfunctions functions.

import argtyper


def footer1(footer: str, hide: bool = False):
    if not hide:
        print(f"-- {footer}")


async def footer2(footer: str, repeat: int = 2):
    print(f"{footer}" * repeat)


# Subcommands are `registered` at the parent
@argtyper.SubCommand(footer1, name="footer_main")
@argtyper.SubCommand(footer2)
def hello(name: str, amount: int = 5):
    print(f"Hello {name.upper()}\n" * amount)


at = argtyper.ArgTyper(hello, arg_defaults={"amount": 2})
at()

This will produce the following output for the parser.

$ python subcommand.py -h
usage: subcommand.py [-h] [--amount AMOUNT] NAME {footer2,footer_main} ...

positional arguments:
  NAME                  provide argument of type STR
  {footer2,footer_main}

optional arguments:
  -h, --help            show this help message and exit
  --amount AMOUNT       provide argument of type INT

SubCommand simply takes two arguments. The first one is the function (either Callable or name:str), the second one is an optional alternative name to be used for this subcommand.

SubParser

The argtyper.SubParser decorator can be used to add additional information to the created subparsers instance. It is basically a wrapper around argparse.ArgumentParser.add_subparsers and takes the same arguments.

import argtyper


def footer1(footer: str, hide: bool = False):
    if not hide:
        print(f"-- {footer}")


async def footer2(footer: str, repeat: int = 2):
    print(f"{footer}" * repeat)


# Hint: The ordering of decorators is irrelevant
@argtyper.SubParser(
    title="Subcommands",
    description="The additional commands to append",
    help="We can run this",
)
# Subcommands are `registered` at the parent
@argtyper.SubCommand(footer1, name="footer_main")
@argtyper.SubCommand(footer2)
def hello(name: str, amount: int = 5):
    print(f"Hello {name.upper()}\n" * amount)


at = argtyper.ArgTyper(hello, arg_defaults={"amount": 2})
at()

This will produce the following output for the parser.

$ python subparser.py -h
usage: subparser.py [-h] [--amount AMOUNT] NAME {footer2,footer_main} ...

positional arguments:
  NAME                  provide argument of type STR

optional arguments:
  -h, --help            show this help message and exit
  --amount AMOUNT       provide argument of type INT

Subcommands:
  The additional commands to append

  {footer2,footer_main}
                        We can run this

The subcommands have now been grouped into their own sections with their own description.

ArgumentGroup

The argtyper.ArgumentGroup decorator can be used to add argument groups to the parser. It takes a list or argument names (as they appear in the function definition) and wraps them with argparse.ArgumentParser.add_argument_group during parser creation.

import argtyper


@argtyper.ArgumentGroup(
    ["firstname", "lastname"],
    title="Name details",
    description="Give your full name here",
)
@argtyper.ArgumentGroup(
    ["nickname", "firstname"],
    title="Nickname details",
    description="Give your Nickname here",
)
@argtyper.Argument(
    "amount", "repetitions", help="How often should we say hello?", metavar="reps"
)
@argtyper.Argument(
    "lastname", "--name", "--n", help="Give me your name", default="Yoda"
)
def hello(nickname: str, firstname: str, lastname: str, amount: int = 2):
    print("\n".join([f"Hello {firstname} '{nickname.upper()}' {lastname}"] * amount))


at = argtyper.ArgTyper(hello)
at()

This will produce the following output for the parser.

$ python argument_group.py -h
usage: argument_group.py [-h] [--name LASTNAME]
                         NICKNAME FIRSTNAME FIRSTNAME REPS

positional arguments:
  REPS                  How often should we say hello?

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

Nickname details:
  Give your Nickname here

  NICKNAME              provide argument of type STR
  FIRSTNAME             provide argument of type STR

Name details:
  Give your full name here

  FIRSTNAME             provide argument of type STR
  --name LASTNAME, --n LASTNAME
                        Give me your name

The arguments have now been grouped into their own sections with their own description.

Warning

There is no check if you put the same argument into different groups. They might then appear more than once on the command line.

MutuallyExclusiveArgumentGroup

The argtyper.MutuallyExclusiveArgumentGroup decorator can be used to add mutually exclusive argument groups to the parser. This wraps argparse.ArgumentParser.add_mutually_exclusive_group.

import argtyper


@argtyper.MutuallyExclusiveArgumentGroup(["foo", "bar"], required=True)
def hello(nickname: str, foo: str = None, bar: str = None):
    print(locals())
    if foo:
        print(f"{nickname} + {foo}")
    else:
        print(f"{bar} + {nickname}")


at = argtyper.ArgTyper(hello)
at()

This will produce the following output for the parser.

$ python exclusive_group.py -h
usage: exclusive_group.py [-h] (--foo FOO | --bar BAR) NICKNAME

positional arguments:
  NICKNAME    provide argument of type STR

optional arguments:
  -h, --help  show this help message and exit
  --foo FOO   provide argument of type STR
  --bar BAR   provide argument of type STR

The arguments have now been grouped into their own sections with their own description.

But now the parser will also make sure that at most one (or exactly one, if required=True) of the arguments in the group is set.

$ python exclusive_group.py supernick --foo FOO --bar BAR
usage: exclusive_group.py [-h] (--foo FOO | --bar BAR) NICKNAME

Error: exclusive_group.py: argument --bar: not allowed with argument --foo

Conclusion

While all decorators have been presented mostly on their own, they can of course also be combined at will.