Skip to content

API Reference

Overview

Need-to-know

In short, only two functions are required to use arguably:

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 acceptable
  • arguably.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 through arguably.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.*()]
):

Exceptions

Additionally, there are two exceptions:

  • arguably.ArguablyException raised if you messed up when setting up arguably
  • arguably.ArguablyWarning passed to warnings.warn() if you messed up when setting up arguably, but not badly. Also used if python3 -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, @arguably.command(alias="h") would alias h to the function that follows.

None
help bool

If False, the help flag -h/--help will not automatically be added to this function.

True

Returns:

Type Description
Callable

If called with parens @arguably.command(...), returns the decorated function. If called without parens @arguably.command, returns the function wrap(func_), which returns func_.

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. $ python3 my/script.py yields script.py, and python3 -m my.script yeilds script.

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 __version__ in the invoked script. If a tuple of one or two strings is passed in, like ("-V", "--ver"), those are used instead of the default --version.

False
strict bool

Will prevent the script from running if there are any ArguablyExceptions raised during CLI initialization.

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 max_help_position in argparse.

60
max_width int

The total maximum width of text to be displayed in the terminal. Equivalent to width in argparse.

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

False if arguably.run() was called and the currently running command is not the targeted command, True in every other case.

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, @arguably.subtype(alias="foo") would cause this class to be built any time an applicable arg is given a string starting with foo,...

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 @arguably.subtype(...), returns the decorated class. If called without parens @arguably.subtype, returns the function wrap(cls_), which returns cls_.

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 Annotated[], stating that this parameter is required.

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 Annotated[], stating that this parameter should be counted.

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 Annotated[], stating that this parameter has a fixed set of choices.

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 Annotated[], stating that this parameter has a special value if the flag is present, but no value is provided.

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 Annotated[], stating that this parameter has a specific handler to call.

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 Annotated[], stating that this parameter should use the builder logic.

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')]