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