Additional Features

ArgTyper provides some additional features, which might be useful in specific circumstances.

Responses

Internally, ArgTyper handles execution of all functions and subfunctions for you. However, sometimes you might need to work with the results from those function calls. For this purpose, ArgTyper allows you to return a List containing the results returned by the respective function calls (by setting return_response=True).

import argtyper


def subfunc():
    return "Just returning more"


@argtyper.SubCommand(subfunc)
async def hello(name: str, amount: int = 2):
    print("\n".join([f"Hello {name.upper()}"] * amount))
    return "We also return here"


at = argtyper.ArgTyper(hello)
responses = at(return_responses=True)

print(f"Responses: {responses}")

This will produce the following output for the parser.

$ python response.py -h
usage: response.py [-h] [--amount AMOUNT] NAME {subfunc} ...

positional arguments:
  NAME             provide argument of type STR
  {subfunc}

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

As can be seen, the response are not printed, since the parser prints the help message and exits. Same happens if the parser would encounter an error during argument parsing. On the other hand, a successfull run would produce this output.

$ python response.py Yoda
Hello YODA
Hello YODA
Responses: ['We also return here']

Calling subcommands will add more entries to the list of returned values:

$ python response.py Yoda subfunc
Hello YODA
Hello YODA
Responses: ['We also return here', 'Just returning more']

Wrapping external methods

Since decorators can also be used as normal functions, and the way ArgTyper is built, it is also quite easy to wrap external/included functions.

import re
import argtyper

argtyper.Argument("pattern", help="The pattern to search for")(re.search)
argtyper.Argument(
    "string", help="The string, in which you want to search for the pattern"
)(re.search)
at = argtyper.ArgTyper(re.search)

responses = at(return_responses=True)
print(responses[0])

This will produce the following output for the parser.

$ python wrap_external.py -h
usage: wrap_external.py [-h] [--flags FLAGS] PATTERN STRING

positional arguments:
  PATTERN        The pattern to search for
  STRING         The string, in which you want to search for the pattern

optional arguments:
  -h, --help     show this help message and exit
  --flags FLAGS  provide argument of type STR

And a successfull run would produce this output.

$ python wrap_external.py 'needle' 'searching for needle in a haystack'
<re.Match object; span=(14, 20), match='needle'>

Subcommands & Instance Methods

When working with classes, registering subcommands does not work exactly the same as for other functions. The problem is, that the functions need to be bound to the correct instances during runtime, in order for self to work correctly.

To support this use case, the argument provided to argtyper.SubCommand to reference a function can not only be passed as Callable, but also as string. The following example will illustrate the difference between the two options.

import argtyper


class Test:
    def __init__(self, value):
        self.value = value

    def test_message(self, message: str):
        print(message)
        print(self.value)

    def get_value(self):
        print(self.value)

    @argtyper.SubCommand(get_value)
    @argtyper.SubCommand("test_message")
    def entry(
        self,
    ):
        ...


t = Test("---- Instance Value ----")
at = argtyper.ArgTyper(t.entry)
at()

The first subcommand simply adds the function get_value, while the second one uses the string test_message to add a subcommand. At first, there won’t be any difference between the two for the help output:

$ python class_method.py -h
usage: class_method.py [-h] {test_message,get_value} ...

positional arguments:
  {test_message,get_value}

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

But the difference becomes apparent as soon as you try to call subcommands. While test_command will work as expected:

$ python class_method.py test_message 'An important message'
An important message
---- Instance Value ----

get_value, which should not take any parameters, will fail because it is missing an argument self:

$ python class_method.py get_value
usage: class_method.py [-h] {test_message,get_value} ...

Error: get_value: the following arguments are required: SELF

To further illustrate the issue, if we actually pass a value to self, execution will fail with an error:

$ python class_method.py get_value something
Traceback (most recent call last):
  File "/home/docs/checkouts/readthedocs.org/user_builds/argtyper/checkouts/latest/docs/scripts/../../examples/class_method.py", line 25, in <module>
    at()
  File "/home/docs/checkouts/readthedocs.org/user_builds/argtyper/envs/latest/lib/python3.9/site-packages/argtyper/__init__.py", line 445, in __call__
    return self._call_interactive(return_responses)
  File "/home/docs/checkouts/readthedocs.org/user_builds/argtyper/envs/latest/lib/python3.9/site-packages/argtyper/__init__.py", line 413, in _call_interactive
    responses = self.call_parser_sync(input_args)
  File "/home/docs/checkouts/readthedocs.org/user_builds/argtyper/envs/latest/lib/python3.9/site-packages/argtyper/__init__.py", line 386, in call_parser_sync
    response = func(**kwargs)
  File "/home/docs/checkouts/readthedocs.org/user_builds/argtyper/checkouts/latest/docs/scripts/../../examples/class_method.py", line 13, in get_value
    print(self.value)
AttributeError: 'str' object has no attribute 'value'

This issues exists, because if we add the function during class creation, we are not adding the bound instance method, and therefore we can’t access self during runtime. However, if the argument is passed as string, ArgTyper will resolve the name during runtime and can therefore reference the correct instance method and execute the function correctly.

Parents=

ArgumentParser also supports a parent= keyword to add arguments from another parser. This is also (somewhat) supported in ArgTyper. For example, if you check the following program:

import argtyper
import argparse


async def sub1(signature: str, hide: bool = False):
    if not hide:
        print(f"-- {signature}")


@argtyper.Command(parents=[argtyper.ArgTyper(sub1).get_parser()], add_help=False)
async def sub2(
    footer: str, debug: bool = False, show: argparse.BooleanOptionalAction = True
):
    print(footer, debug, show)


@argtyper.Command(parents=[argtyper.ArgTyper(sub2).get_parser()], add_help=False)
def hello(**kwargs):
    print("Hello", kwargs)


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

The commands will add the arguments of function sub1 to function sub2. And then those will be added to function hello. Then, if we call help, we can see that the arguments are added to the parser as expected.

$ python parents.py -h
usage: parents.py [-h] [--hide] [--debug] [--show | --no-show]
                  SIGNATURE FOOTER

positional arguments:
  SIGNATURE          provide argument of type STR
  FOOTER             provide argument of type STR

optional arguments:
  -h, --help         show this help message and exit
  --hide             toggle option (default: False)
  --debug            toggle option (default: False)
  --show, --no-show

To actually be able to handle additional arguments, we can simply use **kwargs inside hello, which will then hold the values for the parsed additional arguments.

$ python parents.py 'My Signature' 'the footer' --hide --show
Hello {'signature': 'My Signature', 'hide': True, 'footer': 'the footer', 'debug': False, 'show': True}

Extend the Parser

ArgTyper also gives you access to the underlying parser, in case you want to extend it.

import argtyper

def hello(name: str, amount: int = 2):
    print("\n".join([f"Hello {name.upper()}"] * amount))

at = argtyper.ArgTyper(hello)

parser = at.get_parser()
parser.add_argument('--myversion', action='version', version="Super %(prog)s 1.3.3.7")

at()

This will give you the following output.

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

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
  --myversion      show program's version number and exit