Introduction
funparse
allows you to "derive" an argument parser (such as those from
argparse) from type
annotations of a function's signature, cutting down on the boilerplate code.
It's similar to fire in this way, but
it's more lightweight and designed in a way to give its user more control over
what is going on.
Disclaimer: your user experience may be much richer if you're using a
static type checker, such as mypy
Installation
With pip:
pip install funparse[docstring]
With poetry:
poetry install funparse[docstring]
If you don't need to generate per-argument help strings, you can omit the
[docstring]
extra when installing this package.
Examples
Basic Usage
import sys
import funparse.api as fp
@fp.as_arg_parser
def some_parser_name(
your_name: str,
your_age: int,
pets: list[str] | None = None,
loves_python: bool = False,
) -> None:
print("Hi", your_name)
if pets is not None:
for pet in pets:
print("send greetings to", pet, "for me")
if loves_python:
print("Cool! I love python too!")
some_parser_name.run([
"Johnny",
"33",
*("--pets", "Goofy"),
*("--pets", "Larry"),
*("--pets", "Yes"),
"--loves-python",
])
some_parser_name.run(sys.argv)
Printing Help
import funparse.api as fp
@fp.as_arg_parser
def some_parser_name(
your_name: str,
your_age: int,
) -> None:
print("Hi", your_name)
if your_age > 325:
print("getting elderly, eh")
some_parser_name.print_usage()
some_parser_name.print_help()
usage = some_parser_name.format_usage()
help_str = some_parser_name.format_help()
See more about it
here
Behavior on Booleans
import funparse.api as fp
@fp.as_arg_parser
def booler(
aaa: bool,
bbb: bool = True,
ccc: bool = False,
) -> None:
print(aaa, bbb, ccc)
booler.run([
"yes",
"--bbb",
])
booler.run([
"false",
])
Behavior on Enums
import funparse.api as fp
import enum
class CommandModes(fp.Enum):
CREATE_USER = enum.auto()
LIST_USERS = enum.auto()
DELETE_USER = enum.auto()
@fp.as_arg_parser
def some_parser(mode: CommandModes) -> None:
print(f"you picked {mode.name!r} mode!")
some_parser.run(["CREATE_USER"])
some_parser.run(["create_user"])
some_parser.run(["crEatE_usEr"])
some_parser.run(["NON EXISTING FUNCTIONALITY EXAMPLE"])
Bypassing the command-line
If you want to pass extra data to the function which you're using as your
parser generator, but without having to supply this data through the CLI, you
can use the ignore
parameter on as_arg_parser
, like this:
import funparse.api as fp
@fp.as_arg_parser(ignore=["user_count", "user_name"])
def some_parser(
user_count: int,
user_name: str,
user_address: str,
is_foreigner: bool = False,
) -> None:
print(f"you're the {user_count}th user today! welcome, {user_name}")
print("They say", user_address, "is lovely this time of the year...")
some_parser.with_state(
user_count=33,
user_name="Josh",
).run(["some address..."])
saving_for_later = some_parser.with_state(
user_count=33,
user_name="Josh",
)
saving_for_later.run([
"some address...",
"--is-foreigner",
])
Using custom argument parsers
import argparse
import funparse.api as fp
class MyParser(argparse.ArgumentParser):
"""Just like argparse's, but better!"""
@fp.as_arg_parser(parser_type=MyParser)
def some_parser(
user_name: str,
is_foreigner: bool = False,
) -> None:
print("Welcome", user_name)
if is_foreigner:
print("Nice to have you here")
some_parser.run([
"johnny",
"--is-foreigner",
])
Generating per-argument help strings from docstrings
Thanks to this package, funparse
can generate help
strings for arguments, from the docstring of the function
in question, like this:
import funparse.api as fp
@fp.as_arg_parser(parse_docstring=fp.DocstringStyle.GOOGLE)
def some_parser(
name: str,
is_foreigner: bool = False,
) -> None:
"""My awesome command.
Long description... Aut reiciendis voluptatem aperiam rerum voluptatem non.
Aut sit temporibus in ex ut mollitia. Omnis velit asperiores voluptatem ut
molestiae quis et qui.
Args:
name: some help information about this arg
is_foreigner: some other help information
"""
print("Welcome", name)
if is_foreigner:
print("Nice to have you here")
some_parser.print_help()
The generated command help should look like this:
usage: - [-h] [--is-foreigner] name
Long description... Aut reiciendis voluptatem aperiam rerum voluptatem non.
Aut sit temporibus in ex ut mollitia. Omnis velit asperiores voluptatem ut
molestiae quis et qui.
positional arguments:
name some help information about this arg
options:
-h, --help show this help message and exit
--is-foreigner bool (default=False): some other help information
Generating per-argument help strings type annotations
As of PEP 727, there's a new way to document information for parameters, aside
from docstrings, which is the Doc(...)
type, supposed to be used with
typing.Annotated
. This way of documenting is supported by funparse, so that
you can auto-generate help string for each argument, from these annotations,
like the example below shows:
import funparse.api as fp
from typing import Annotated
from typing_extensions import Doc
@fp.as_arg_parser(parse_docstring=fp.DocstringStyle.GOOGLE)
def some_parser_name(
param_1: int,
param_2: int,
param_3: Annotated[int, Doc("this is only documented here")],
param_4: Annotated[int, Doc("this is documented here and in the docstring")],
) -> None:
"""Some short description
Some long description Dolorem ut illum in dolorum eaque maxime dignissimos.
Tempora provident eum sit. Modi voluptatibus dignissimos occaecati qui
quisquam minus quis et.
Args:
param_2: this is only documented in the docstring
param_4: this is documented both in the docstring and as an annotation
"""
print(param_1)
print(param_2)
print(param_3)
print(param_4)
some_parser_name.print_help()
Variadic Positional Arguments
You can use the star notation in a function's signature to specify that the
argument in question should take in one or more parameters. If you want your
function's parameter to allow zero or more items, consider defining, in your
functions signature, a parameter of type list[T] | None
with a default value
of None
, as shown in './examples/01_basic_usage.py' or in the above section
titled "Basic Usage".
import funparse.api as fp
@fp.as_arg_parser
def some_parser_name(
*pet_names: str,
your_name: str = "John",
) -> None:
print("Hi", your_name)
for pet_name in pet_names:
print("send greetings to", pet_name, "for me")
some_parser_name.run([
"Goofy",
"Larry",
"Yes",
"--your-name",
"Johnny",
])
Beyond as_arg_parser
, this module also ships:
funparse.Enum
, which is a subclass of enum.Enum
, but with a __str__
that better fits your CLI appsfunparse.ArgumentParser
, which is a subclass of argparse.ArgumentParser
that, unlike the latter, does not terminate your app on (most) exceptions
Have fun!