API Reference¶
Overview¶
Need-to-know¶
In short, only two functions are required to use arguably
:
@arguably.command
to mark which functions to put on the CLIarguably.run()
parses the CLI arguments and calls the marked functions
Extras¶
The rest of the functions aren't necessary except in specific use cases:
arguably.error()
lets you error out if an input is the correct type but isn't acceptablearguably.is_target()
tells you if the targeted command is being run, or if one of its ancestors is being run@arguably.subtype
marks a subclass as buildable througharguably.arg.builder()
Special behaviors¶
There are a number of special behaviors you can attach to a parameter. These utilize the ability to attach metadata to
a type using typing.Annotated[]
:
def foo(
param: Annotated[<param_type>, arguably.arg.*()]
):
arguably.arg.required()
requireslist[]
and*args
params to not be empty, or marks an--option
as required.arguably.arg.count()
counts the number of times an option appears:-vvvv
gives4
.arguably.arg.choices(*choices)
restricts inputs tochoices
arguably.arg.missing(omit_value)
--option foo
yieldsfoo
, but this allows the value to be omitted: just--option
will use the givenomit_value
.arguably.arg.handler(func)
skips all the argument processingarguably
does and just callsfunc
arguably.arg.builder()
treats the input as instructions on how to build a class
Exceptions¶
Additionally, there are two exceptions:
arguably.ArguablyException
raised if you messed up when setting uparguably
arguably.ArguablyWarning
passed towarnings.warn()
if you messed up when setting uparguably
, but not badly. Also used ifpython3 -m arguably <script.py>
is used, but there were some problems running the script.
arguably¶
arguably.command ¶
command(func=None, /, *, alias=None, help=True)
Mark a function as a command that should appear on the CLI. If multiple functions are decorated with this, they will all be available as subcommands. If only one function is decorated, it is automatically selected - no need to specify it on the CLI.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
func |
Optional[Callable]
|
The target function. |
None
|
alias |
Optional[str]
|
An alias for this function. For example, |
None
|
help |
bool
|
If |
True
|
Returns:
Type | Description |
---|---|
Callable
|
If called with parens |
Examples:
#!/usr/bin/env python3
import arguably
@arguably.command
def some_function(required, not_required=2, *others: int, option: float = 3.14):
"""
this function is on the command line!
Args:
required: a required argument
not_required: this one isn't required, since it has a default value
*others: all the other positional arguments go here
option: [-x] keyword-only args are options, short name is in brackets
"""
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 intro.py -h
usage: intro.py [-h] [-x OPTION] required [not-required] [others ...]
this function is on the command line!
positional arguments:
required a required argument (type: str)
not-required this one isn't required, since it has a default value (type: int, default: 2)
others all the other positional arguments go here (type: int)
options:
-h, --help show this help message and exit
-x, --option OPTION keyword-only args are options, short name is in brackets (type: float, default: 3.14)
Or, with multiple commands:
#!/usr/bin/env python3
import arguably
@arguably.command(alias="f")
def first(): ...
@arguably.command(alias="s")
def second(): ...
@arguably.command
def second__subcmd1(): ...
def second__subcmd2(): ...
arguably.command(second__subcmd2) # Can also be invoked this way
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 command.py -h
usage: command-example-2.py [-h] command ...
positional arguments:
command
first (f)
second (s)
options:
-h, --help show this help message and exit
user@machine:~$ python3 command.py s -h
usage: command-example-2.py second [-h] command ...
positional arguments:
command
subcmd1
subcmd2
options:
-h, --help show this help message and exit
arguably.run ¶
run(
name=None,
always_subcommand=False,
version_flag=False,
strict=True,
show_defaults=True,
show_types=True,
max_description_offset=60,
max_width=120,
command_metavar="command",
output=None,
)
Set up the argument parser, parse argv, and run the appropriate command(s)
Parameters:
Name | Type | Description | Default |
---|---|---|---|
name |
Optional[str]
|
Name of the script/program. Defaults to the filename or module name, depending on how the script is
run. |
None
|
always_subcommand |
bool
|
If true, will force a subcommand interface to be used, even if there's only one command. |
False
|
version_flag |
Union[bool, Tuple[str], Tuple[str, str]]
|
If true, adds an option to show the script version using the value of |
False
|
strict |
bool
|
Will prevent the script from running if there are any |
True
|
show_defaults |
bool
|
Show the default value (if any) for each argument at the end of its help string. |
True
|
show_types |
bool
|
Show the type of each argument at the end of its help string. |
True
|
max_description_offset |
int
|
The maximum number of columns before argument descriptions are printed. Equivalent
to |
60
|
max_width |
int
|
The total maximum width of text to be displayed in the terminal. Equivalent to |
120
|
command_metavar |
str
|
The name shown in the usage string for taking in a subcommand. Change this if you have a conflicting argument name. |
'command'
|
output |
Optional[TextIO]
|
Where argparse output should be written - can write to a file, stderr, or anything similar. |
None
|
Returns:
Type | Description |
---|---|
Any
|
The return value from the called function. |
Examples:
#!/usr/bin/env python3
"""description for this script"""
from io import StringIO
import arguably
__version__ = "1.2.3"
@arguably.command
def example(): ...
if __name__ == "__main__":
output = StringIO()
try:
arguably.run(
name="myname",
always_subcommand=True,
version_flag=True,
command_metavar="mycmd",
output=output
)
finally:
print(f"Captured output length: {len(output.getvalue())=}")
print()
print(output.getvalue(), end="")
user@machine:~$ python3 run.py -h
Captured output length: len(output.getvalue())=222
usage: myname [-h] [--version] mycmd ...
description for this script
positional arguments:
mycmd
example
options:
-h, --help show this help message and exit
--version show program's version number and exit
user@machine:~$ python3 run.py --version
Captured output length: len(output.getvalue())=13
myname 1.2.3
arguably.error ¶
error(message)
Prints an error message and exits. Should be used when a CLI input is not of the correct form. arguably
handles converting values to the correct type, but if extra validation is performed and fails, you should call
this.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
message |
str
|
A message to be printed to the console indicating why the input is wrong. |
required |
Raises:
Type | Description |
---|---|
SystemExit
|
The script will exit. |
Examples:
#!/usr/bin/env python3
import arguably
@arguably.command
def high_five(*people):
if len(people) > 5:
arguably.error("Too many people to high-five!")
for person in people:
print(f"High five, {person}!")
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 error.py Graham John Terry Eric Terry Michael
usage: error.py [-h] [people ...]
error.py: error: Too many people to high-five!
arguably.is_target ¶
is_target()
Only useful if invoke_ancestors=True
. Returns True
if the targeted command is being executed and False
if
not. This is safe to call even if arguably
is not being used, since it returns True
if arguably.run()
is
not being used.
Returns:
Type | Description |
---|---|
bool
|
|
Examples:
import arguably
@arguably.command
def __root__(*, config_file=None):
print(f"Using config {config_file}")
if not arguably.is_target():
return
print("__root__ is the target!")
@arguably.command
def hi():
print("hi is the target!")
@arguably.command
def bye():
print("bye is the target!")
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 is_target.py --config-file foo.yml
Using config foo.yml
__root__ is the target!
user@machine:~$ python3 is_target.py --config-file foo.yml hi
Using config foo.yml
hi is the target!
arguably.subtype ¶
subtype(cls=None, /, *, alias, factory=None)
Mark a decorated class as a subtype that should be buildable for a parameter using arg.builder(). The alias parameter is required.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
cls |
Optional[type]
|
The target class. |
None
|
alias |
str
|
An alias for this class. For example, |
required |
factory |
Callable | None
|
What should be called to actually build the subtype. This should only be needed if the default behavior doesn't work. |
None
|
Returns:
Type | Description |
---|---|
Union[Callable[[type], type], type]
|
If called with parens |
Examples:
import arguably
from dataclasses import dataclass
from typing import Annotated
class Nic: ...
@arguably.subtype(alias="tap")
@dataclass
class TapNic(Nic):
model: str
@dataclass
class UserNic(Nic):
hostfwd: str
arguably.subtype(UserNic, alias="user") # Can also be called like this
@arguably.command
def qemu_style(*, nic: Annotated[list[Nic], arguably.arg.builder()]):
print(f"{nic=}")
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 subtype.py --nic tap,model=e1000 --nic user,hostfwd=tcp::10022-:22
nic=[TapNic(model='e1000'), UserNic(hostfwd='tcp::10022-:22')]
arguably.ArguablyException ¶
ArguablyException()
Bases: Exception
Raised when a decorated function is incorrectly set up in some way. Will not be raised when a user provides incorrect input to the CLI.
Examples:
#!/usr/bin/env python3
import arguably
@arguably.command
def example(collision_, _collision):
print("You should never see this")
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 arguably-exception.py
Traceback (most recent call last):
File ".../arguably/etc/scripts/api-examples/arguably-exception.py", line 9, in <module>
arguably.run()
File ".../arguably/arguably/_context.py", line 706, in run
cmd = self._process_decorator_info(command_decorator_info)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../arguably/arguably/_context.py", line 284, in _process_decorator_info
return Command(
^^^^^^^^
File "<string>", line 9, in __init__
File ".../arguably/arguably/_commands.py", line 214, in __post_init__
raise util.ArguablyException(
arguably._util.ArguablyException: Function argument `_collision` in `example` conflicts with `collision_`, both
names simplify to `collision`
arguably.ArguablyWarning ¶
ArguablyWarning()
Bases: UserWarning
If strict checks are disabled through arguably.run(strict=False)
this is emitted when a decorated function is
incorrectly set up in some way, but arguably can continue. Will not be raised when a user provides incorrect input
to the CLI.
When arguably
is directly invoked through python3 -m arguably ...
, strict=False
is always set.
Note that this is a warning - it is used with warnings.warn
.
Examples:
def example_failed(collision_, _collision):
print("You should never see this")
def example_ok():
print("All good")
user@machine:~$ python3 -m arguably arguably-warn.py -h
.../arguably/etc/scripts/api-examples/arguably-warn.py:1: ArguablyWarning: Unable to add function
example_failed: Function argument `_collision` in `example-failed` conflicts with `collision_`, both names
simplify to `collision`
def example_failed(collision_, _collision):
usage: arguably-warn [-h] command ...
positional arguments:
command
example-ok
options:
-h, --help show this help message and exit
arguably.arg¶
A collection of methods for adding a modifier to a parameter. Should be used in Annotated[]
.
Examples:
def foo(
*,
verbose: Annotated[int, arguably.arg.count()],
):
arguably.arg.required ¶
required()
Marks a field as required. For *args
or a list[]
, requires at least one item.
Returns:
Type | Description |
---|---|
RequiredModifier
|
A value for use with |
Examples:
import arguably
from typing import Annotated
@arguably.command
def email(
from_: str,
*to: Annotated[str, arguably.arg.required()]
):
print(f"{from_=}", f"{to=}")
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 arg-required.py -h
usage: arg-required.py [-h] from to [to ...]
positional arguments:
from (type: str)
to (type: str)
options:
-h, --help show this help message and exit
user@machine:~$ python3 arg-required.py sender@example.com
usage: arg-required.py [-h] from to [to ...]
arg-required.py: error: the following arguments are required: to
arguably.arg.count ¶
count()
Counts the number of times a flag is given. For example, -vvvv
would yield 4
.
Returns:
Type | Description |
---|---|
CountedModifier
|
A value for use with |
Examples:
import arguably
from typing import Annotated
@arguably.command
def process(
*,
verbose: Annotated[int, arguably.arg.count()],
):
"""
:param verbose: [-v] verbosity
"""
print(f"{verbose=}")
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 arg-count.py -vvvv
verbose=4
arguably.arg.choices ¶
choices(*choices)
Specifies a fixed set of values that a parameter is allowed to be. If a parameter is an enum.Enum
type, this
logic is already used to restrict the inputs to be one of the enum entries.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
*choices |
Union[str, Enum]
|
The allowed values. Must all be of the same type, and be compatible with the annotated type for this parameter. |
()
|
Returns:
Type | Description |
---|---|
ChoicesModifier
|
A value for use with |
Examples:
import arguably
from typing import Annotated
@arguably.command
def move(
direction: Annotated[str, arguably.arg.choices("left", "right", "up", "down")]
):
"""An enum is usually recommended for cases like this"""
print(f"{direction=}")
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 arg-choices.py north
usage: arg-choices.py [-h] {left,right,up,down}
arg-choices.py: error: argument direction: invalid choice: 'north' (choose from 'left', 'right', 'up', 'down')
arguably.arg.missing ¶
missing(omit_value)
Allows an option to be specified, but its value be omitted. In the case where the value is given, the value is
used, but if it is omitted, omit_value
will be used.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
omit_value |
str
|
The value that will be used if the flag is present, but the value is omitted. |
required |
Returns:
Type | Description |
---|---|
MissingArgDefaultModifier
|
A value for use with |
Examples:
import arguably
from pathlib import Path
from typing import Annotated
@arguably.command
def do_something(
*,
log: Annotated[Path | None, arguably.arg.missing("~/.log.txt")] = None
):
print(f"{log=}")
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 arg-missing.py
log=None
user@machine:~$ python3 arg-missing.py --log
log=PosixPath('~/.log.txt')
user@machine:~$ python3 arg-missing.py --log foo.log
log=PosixPath('foo.log')
arguably.arg.handler ¶
handler(func)
Causes a user-provided handler to be used to process the input string, instead of trying to process it using the types from type annotations.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
func |
Callable[[str], Any]
|
The function to call to process the input string. |
required |
Returns:
Type | Description |
---|---|
HandlerModifier
|
A value for use with |
Examples:
import arguably
from typing import Annotated
@arguably.command
def handle_it(
version: Annotated[int, arguably.arg.handler(lambda s: int(s.split("-")[-1]))] = None
):
print(f"{version=}")
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 arg-handler.py Python-3
version=3
arguably.arg.builder ¶
builder()
Causes the arguably builder logic to be used instead of trying to instantiate the type from the input string.
Returns:
Type | Description |
---|---|
BuilderModifier
|
A value for use with |
Examples:
import arguably
from dataclasses import dataclass
from typing import Annotated
class Nic: ...
@arguably.subtype(alias="tap")
@dataclass
class TapNic(Nic):
model: str
@arguably.subtype(alias="user")
@dataclass
class UserNic(Nic):
hostfwd: str
@arguably.command
def qemu_style(*, nic: Annotated[list[Nic], arguably.arg.builder()]):
print(f"{nic=}")
if __name__ == "__main__":
arguably.run()
user@machine:~$ ./build.py --nic tap,model=e1000 --nic user,hostfwd=tcp::10022-:22
nic=[TapNic(model='e1000'), UserNic(hostfwd='tcp::10022-:22')]