App

This module provides base functionality to build CLI interfaces.

Creating and running an app

Yuio’s CLI applications have functional interface. Decorate main function with the app() decorator, and use App.run() method to start it:

# Let's define an app with one flag and one positional argument.
@app
def main(
    #: help message for `arg`
    arg: str,  # [1]_
    /,
    *,
    #: help message for `--flag`
    flag: int = 0  # [2]_
):
    """this command does a thing"""
    yuio.io.info("flag=%r, arg=%r", flag, arg)

if __name__ == "__main__":
    # We can now use `main.run` to parse arguments and invoke `main`.
    # Notice that `run` does not return anything. Instead, it terminates
    # python process with an appropriate exit code.
    main.run("--flag 10 foobar!".split())
  1. Positional-only arguments become positional CLI options.

  2. Other arguments become CLI flags.

Function’s arguments will become program’s flags and positionals, and function’s docstring will become app’s description.

Help messages for the flags are parsed from line comments right above the field definition (comments must start with #:). They are all formatted using Markdown or RST depending on App.doc_format.

Parsers for CLI argument values are derived from type hints. Use the parser parameter of the field() function to override them.

Arguments with bool parsers and parsers that support parsing collections are handled to provide better CLI experience:

@app
def main(
    # Will create flags `--verbose` and `--no-verbose`.
    # Since default is `False`, `--no-verbose` will be hidden from help
    # to reduce clutter.
    verbose: bool = False,
    # Will create a flag with `nargs=*`: `--inputs path1 path2 ...`
    inputs: list[pathlib.Path] = [],
): ...
yuio.app.app(
command: Callable[[...], None | bool] | None = None,
/,
*,
prog: str | None = None,
usage: str | None = None,
description: str | None = None,
epilog: str | None = None,
allow_abbrev: bool = False,
subcommand_required: bool = True,
setup_logging: bool = True,
theme: Theme | Callable[[Term], Theme] | None = None,
version: str | None = None,
bug_report: ReportSettings | bool = False,
is_dev_mode: bool | None = None,
doc_format: Literal['md', 'rst'] | DocParser | None = None,
) Any

Create an application.

This is a decorator that’s supposed to be used on the main method of the application. This decorator returns an App object.

Parameters:
  • command – the main function of the application.

  • prog – overrides program’s name, see App.prog.

  • usage – overrides program’s usage description, see App.usage.

  • description – overrides program’s description, see App.description.

  • epilog – overrides program’s epilog, see App.epilog.

  • allow_abbrev – whether to allow abbreviating unambiguous flags, see App.allow_abbrev.

  • subcommand_required – whether this app requires a subcommand, see App.subcommand_required.

  • setup_logging – whether to perform basic logging setup on startup, see App.setup_logging.

  • theme – overrides theme that will be used when setting up yuio.io, see App.theme.

  • version – program’s version, will be displayed using the --version flag.

  • bug_report – settings for automated bug report generation. If present, adds the --bug-report flag.

  • is_dev_mode – enables additional logging, see App.is_dev_mode.

  • doc_format – overrides program’s documentation format, see App.doc_format.

Returns:

an App object that wraps the original function.

final class yuio.app.App(
command: C,
/,
*,
prog: str | None = None,
usage: str | None = None,
help: str | Disabled | None = None,
description: str | None = None,
epilog: str | None = None,
subcommand_required: bool = True,
allow_abbrev: bool = False,
setup_logging: bool = True,
theme: Theme | Callable[[Term], Theme] | None = None,
version: str | None = None,
bug_report: ReportSettings | bool = False,
is_dev_mode: bool | None = None,
doc_format: Literal['md', 'rst'] | DocParser | None = None,
)

A class that encapsulates app settings and logic for running it.

It is better to create instances of this class using the app() decorator, as it provides means to decorate the main function and specify all of the app’s parameters.

run(args: list[str] | None = None) NoReturn

Parse arguments, set up yuio.io and logging, and run the application.

Parameters:

args – command line arguments. If none are given, use arguments from sys.argv.

Returns:

this method does not return, it exits the program instead.

wrapped(...)

The original callable what was wrapped by app().

Configuring CLI arguments

Names and types of arguments are determined by names and types of the app function’s arguments. You can use the field() function to override them:

yuio.app.field(
*,
default: Any = yuio.MISSING,
parser: Parser[Any] | None = None,
env: str | Disabled | None = None,
flags: str | list[str] | Positional | Disabled | None = None,
required: bool | None = None,
completer: Completer | None | Missing = yuio.MISSING,
merge: Callable[[Any, Any], Any] | None = None,
mutex_group: MutuallyExclusiveGroup | None = None,
option_ctor: Callable[[...], Any] | None = None,
help: str | Disabled | None = None,
help_group: HelpGroup | Collapse | None | Missing = yuio.MISSING,
metavar: str | None = None,
usage: Collapse | bool | None = None,
default_desc: str | None = None,
show_if_inherited: bool | None = None,
) Any

Field descriptor, used for additional configuration of CLI options and config fields.

Parameters:
  • default – default value for the field or CLI option.

  • parser – parser that will be used to parse config values and CLI options.

  • env

    specifies name of environment variable that will be used if loading config from environment.

    Pass DISABLED to disable loading this field form environment.

    In sub-config fields, controls prefix for all environment variables within this sub-config; pass an empty string to disable prefixing.

  • flags

    list of names (or a single name) of CLI flags that will be used for this field.

    In configs, pass DISABLED to disable loading this field form CLI arguments.

    In sub-config fields, controls prefix for all flags withing this sub-config; pass an empty string to disable prefixing.

  • completer – completer that will be used for autocompletion in CLI. Using this option is equivalent to overriding completer with yuio.parse.WithMeta.

  • merge – defines how values of this field are merged when configs are updated.

  • mutex_group – defines mutually exclusive group for this field.

  • option_ctor

    this parameter is similar to argparse‘s action: it allows overriding logic for handling CLI arguments by providing a custom Option implementation.

    option_ctor should be a callable which takes a single positional argument of type OptionSettings, and returns an instance of yuio.cli.Option.

  • help

    help message that will be used in CLI option description, formatted using RST or Markdown (see App.doc_format).

    Pass yuio.DISABLED to remove this field from CLI help.

  • help_group

    overrides group in which this field will be placed when generating CLI help message.

    Pass yuio.COLLAPSE to create a collapsed group.

  • metavar – value description that will be used for CLI help messages. Using this option is equivalent to overriding desc with yuio.parse.WithMeta.

  • usage

    controls how this field renders in CLI usage section.

    Pass False to remove this field from usage.

    Pass yuio.COLLAPSE to omit this field and add a single string <options> instead.

    Setting usage on sub-config fields overrides default usage for all fields within this sub-config.

  • default_desc

    overrides description for default value in CLI help message.

    Pass an empty string to hide default value.

  • show_if_inherited – for fields with flags, enables showing this field in CLI help message for subcommands.

Returns:

a magic object that will be replaced with field’s default value once a new config class is created.

Example:

In apps:

@yuio.app.app
def main(
    # Will be loaded from `--input`.
    input: pathlib.Path | None = None,
    # Will be loaded from `-o` or `--output`.
    output: pathlib.Path | None = field(
        default=None, flags=["-o", "--output"]
    ),
): ...

In configs:

class AppConfig(Config):
    model: pathlib.Path | None = field(
        default=None,
        help="trained model to execute",
    )
yuio.app.inline(
help: str | Disabled | None = None,
help_group: HelpGroup | Collapse | None | Missing = yuio.MISSING,
usage: Collapse | bool | None = None,
show_if_inherited: bool | None = None,
) Any

A shortcut for inlining nested configs.

Equivalent to calling field() with env and flags set to an empty string.

Using configs in CLI

You can use Config as a type of an app function’s parameter. This will make all of config fields into flags as well. By default, Yuio will use parameter name as a prefix for all fields in the config; you can override it with field() or inline():

class KillCmdConfig(yuio.config.Config):
    signal: int
    pid: int = field(flags=["-p", "--pid"])


@app
def main(
    kill_cmd: KillCmdConfig,  # [1]_
    kill_cmd_2: KillCmdConfig = field(flags="--kill"),  # [2]_
    kill_cmd_3: KillCmdConfig = field(flags=""),  # [3]_
): ...
  1. kill_cmd.signal will be loaded from --kill-cmd-signal.

  2. copy_cmd_2.signal will be loaded from --kill-signal.

  3. kill_cmd_3.signal will be loaded from --signal.

Note

Positional arguments are not allowed in configs, only in apps.

App settings

You can override default usage and help messages as well as control some of the app’s help formatting using its arguments:

class yuio.app.App
prog: str | None

Program’s primary name.

For main app, this attribute controls its display name and generation of shell completion scripts.

For subcommands, this attribute is ignored.

By default, inferred from sys.argv.

usage: str

Program or subcommand synapsis.

This string will be processed using the to bash syntax, and then it will be %-formatted with a single keyword argument prog. If command supports multiple signatures, each of them should be listed on a separate string. For example:

@app
def main(): ...

main.usage = """
%(prog)s [-q] [-f] [-m] [<branch>]
%(prog)s [-q] [-f] [-m] --detach [<branch>]
%(prog)s [-q] [-f] [-m] [--detach] <commit>
...
"""

By default, usage is generated from CLI flags.

description: str

Text that is shown before CLI flags help, usually contains short description of the program or subcommand.

The text should be formatted using Markdown or RST, depending on doc_format. For example:

@yuio.app.app(doc_format="md")
def main(): ...

main.description = """
This command does a thing.

# Different ways to do a thing

This command can apply multiple algorithms to achieve
a necessary state in which a thing can be done. This includes:

- randomly turning the screen on and off;

- banging a head on a table;

- fiddling with your PCs power cord.

By default, the best algorithm is determined automatically.
However, you can hint a preferred algorithm via the `--hint-algo` flag.

"""

By default, inferred from command’s docstring.

help: str | Literal[_Placeholders.DISABLED]

Short help message that is shown when listing subcommands.

By default, uses first paragraph of description.

epilog: str

Text that is shown after the main portion of the help message.

The text should be formatted using Markdown or RST, depending on doc_format.

subcommand_required: bool

Require the user to provide a subcommand for this command.

If this command doesn’t have any subcommands, this option is ignored.

Enabled by default.

allow_abbrev: bool

Allow abbreviating CLI flags if that doesn’t create ambiguity.

Disabled by default.

Note

This attribute should be set in the root app; it is ignored in subcommands.

setup_logging: bool

If True, the app will call logging.basicConfig() during its initialization. Disable this if you want to customize logging initialization.

Disabling this option also removes the --verbose flag form the CLI.

Note

This attribute should be set in the root app; it is ignored in subcommands.

theme: Theme | Callable[[Term], Theme] | None

A custom theme that will be passed to yuio.io.setup() on application startup.

Note

This attribute should be set in the root app; it is ignored in subcommands.

version: str | None

If not None, add --version flag to the CLI.

Note

This attribute should be set in the root app; it is ignored in subcommands.

bug_report: ReportSettings | bool

If not False, add --bug-report flag to the CLI.

This flag automatically collects data about environment and prints it in a format suitable for adding to a bug report.

Note

This attribute should be set in the root app; it is ignored in subcommands.

is_dev_mode: bool | None

If True, this will enable logging.captureWarnings() and configure internal Yuio logging to show warnings.

By default, dev mode is detected by checking if version contains substring "dev".

Tip

You can always enable full debug logging by setting environment variable YUIO_DEBUG.

If enabled, full log will be saved to YUIO_DEBUG_FILE.

Note

This attribute should be set in the root app; it is ignored in subcommands.

doc_format: Literal['md', 'rst'] | DocParser

Format or parser that will be used to interpret documentation.

Note

This attribute should be set in the root app; it is ignored in subcommands.

Creating sub-commands

You can create multiple sub-commands for the main function using the App.subcommand() method:

@app
def main(): ...


@main.subcommand
def do_stuff(): ...

There is no limit to how deep you can nest subcommands, but for usability reasons we suggest not exceeding level of sub-sub-commands (git stash push, anyone?)

When user invokes a subcommand, the main() function is called first, then subcommand. In the above example, invoking our app with subcommand push will cause main() to be called first, then push().

This behavior is useful when you have some global configuration flags attached to the main() command. See the example app for details.

class yuio.app.App
subcommand(
cb: Callable[[...], None | bool] | App[Any] | None = None,
/,
*,
name: str | None = None,
aliases: list[str] | None = None,
usage: str | None = None,
help: str | Disabled | None = None,
description: str | None = None,
epilog: str | None = None,
subcommand_required: bool = True,
) Any

Register a subcommand for the given app.

This method can be used as a decorator, similar to the app() function.

Parameters:
  • name – allows overriding subcommand’s name.

  • aliases – allows adding alias names for subcommand.

  • usage – overrides subcommand’s usage description, see App.usage.

  • help – overrides subcommand’s short help, see App.help. pass DISABLED to hide this subcommand in CLI help message.

  • description – overrides subcommand’s description, see App.description.

  • epilog – overrides subcommand’s epilog, see App.epilog.

  • subcommand_required – whether this subcommand requires another subcommand, see App.subcommand_required.

Returns:

a new App object for a subcommand.

lazy_subcommand(
path: str,
name: str,
/,
*,
aliases: list[str] | None = None,
help: str | Disabled | None = None,
)

Add a subcommand for this app that will be imported and loaded on demand.

Parameters:
  • path

    dot-separated path to a command or command’s main function.

    As a hint, module can be separated from the rest of the path with a semicolon, i.e. "module.submodule:class.method".

  • name – subcommand’s primary name.

  • aliases – allows adding alias names for subcommand.

  • help – allows specifying subcommand’s help. If given, generating CLI help for base command will not require importing subcommand.

Example:

In module my_app.commands.run:

import yuio.app


@yuio.app.app
def command(): ...

In module my_app.main:

import yuio.app


@yuio.app.app
def main(): ...


main.lazy_subcommand("my_app.commands.run:command", "run")

Controlling how sub-commands are invoked

By default, if a command has sub-commands, the user is required to provide a sub-command. This behavior can be disabled by setting App.subcommand_required to False.

When this happens, we need to understand whether a subcommand was invoked or not. To determine this, you can accept a special parameter called _command_info of type CommandInfo. It will contain info about the current function, including its name and subcommand:

@app
def main(_command_info: CommandInfo):
    if _command_info.subcommand is not None:
        # A subcommand was invoked.
        ...

You can call the subcommand on your own by using _command_info.subcommand as a callable:

@app
def main(_command_info: CommandInfo):
    if _command_info.subcommand is not None and ...:
        _command_info.subcommand()  # manually invoking a subcommand

If you wish to disable calling the subcommand, you can return False from the main function:

@app
def main(_command_info: CommandInfo):
    ...
    # Subcommand will not be invoked.
    return False
final class yuio.app.CommandInfo(
name: str,
command: App[Any],
namespace: ConfigNamespace[_CommandConfig],
)

Data about the invoked command.

name

Name of the current command.

If it was invoked by alias, this will contains the primary command name.

For the main function, the name will be set to "__main__".

property subcommand: CommandInfo | None

Subcommand of this command, if one was given.

Handling options with multiple values

When you create an option with a container type, Yuio enables passing its values by specifying multiple arguments. For example:

@yuio.app.app
def main(list: list[int]):
    print(list)

Here, you can pass values to --list as separate arguments:

$ app --list 1 2 3
[1, 2, 3]

If you specify value for --list inline, it will be handled as a delimiter-separated list:

$ app --list='1 2 3'
[1, 2, 3]

This allows resolving ambiguities between flags and positional arguments:

$ app --list='1 2 3' subcommand

Technically, --list 1 2 3 causes Yuio to invoke list_parser.parse_many(["1", "2", "3"]), while --list='1 2 3' causes Yuio to invoke list_parser.parse("1 2 3").

Handling flags with optional values

When designing a CLI, one important question is how to handle flags with optional values, if at all. There are several things to consider:

  1. Does a flag have clear and predictable behavior when its value is not specified?

    For boolean flags the default behavior is obvious: --use-gpu will enable GPU, i.e. it is equivalent to --use-gpu=true.

    For flags that accept non-boolean values, though, things get messier. What will a flag like --n-threads do? Will it calculate number of threads based on available CPU cores? Will it use some default value?

    In these cases, it is usually better to require a sentinel value: --n-threads=auto.

  2. Where should flag’s value go, it it’s provided?

    We can only allow passing value inline, i.e. --use-gpu=true. Or we can greedily take the following argument as flag’s value, i.e. --use-gpu true.

    The later approach has a significant downside: we don’t know whether the next argument was intended for the flag or for a free-standing option.

    For example:

    $ my-renderer --color true  # is `true` meant for `--color`,
    $                           # or is it a subcommand for `my-renderer`?
    

Here’s how Yuio handles this dilemma:

  1. High level API does not allow creating flags with optional values.

    To create one, you have to make a custom implementation of yuio.cli.Option and set its allow_no_args to True. This will correspond to the greedy approach.

    Note

    Positionals with defaults are treated as optional because they don’t create ambiguities.

  2. Boolean flags allow specifying value inline, but not as a separate argument.

  3. Yuio does not allow passing inline values to short boolean flags without adding an equals sign. For example, -ftrue will not work, while -f=true will.

    This is done to enable grouping short flags: ls -laH should be parsed as ls -l -a -H, not as ls -l=aH.

  4. On lower levels of API, Yuio allows precise control over this behavior by setting Option.nargs, Option.allow_no_args, Option.allow_inline_arg, and Option.allow_implicit_inline_arg.

Creating custom CLI options

You can override default behavior and presentation of a CLI option by passing custom option_ctor to field(). Furthermore, you can create your own implementation of yuio.cli.Option to further fine-tune how an option is parsed, presented in CLI help, etc.

yuio.app.bool_option(*, neg_flags: list[str] | None = None) OptionCtor[bool]

Factory for yuio.cli.BoolOption.

Parameters:

neg_flags – additional set of flags that will set option’s value to False. If not given, a negative flag will be created by adding prefix no- to the first long flag of the option.

Example:

Boolean flag --json implicitly creates flag --no-json:

@yuio.app.app
def main(
    json: bool = yuio.app.field(
        default=False,
        option_ctor=yuio.app.bool_option(),
    ),
): ...

Boolean flag --json with explicitly provided flag --disable-json:

@yuio.app.app
def main(
    json: bool = yuio.app.field(
        default=False,
        option_ctor=yuio.app.bool_option(
            neg_flags=["--disable-json"],
        ),
    ),
): ...
yuio.app.parse_one_option() OptionCtor[Any]

Factory for yuio.cli.ParseOneOption.

This option takes one argument and passes it to Parser.parse().

Example:

Forcing a field which can use parse_many_option() to use parse_one_option() instead.

@yuio.app.app
def main(
    files: list[str] = yuio.app.field(
        default=[],
        parser=yuio.parse.List(yuio.parse.Int(), delimiter=","),
        option_ctor=yuio.app.parse_one_option(),
    ),
): ...

This will disable multi-argument syntax:

$ prog --files a.txt,b.txt  # Ok
$ prog --files a.txt b.txt  # Error: `--files` takes one argument.
yuio.app.parse_many_option() OptionCtor[Any]

Factory for yuio.cli.ParseManyOption.

This option takes multiple arguments and passes them to Parser.parse_many().

yuio.app.store_const_option(const: T) OptionCtor[T]

Factory for yuio.cli.StoreConstOption.

This options takes no arguments. When it’s encountered amongst CLI arguments, it writes const to the resulting config.

yuio.app.count_option() OptionCtor[int]

Factory for yuio.cli.CountOption.

This option counts number of times it’s encountered amongst CLI arguments.

Equivalent to using store_const_option() with const=1 and merge=lambda a, b: a + b.

Example:

@yuio.app.app
def main(
    quiet: int = yuio.app.field(
        default=0,
        flags=["-q", "--quiet"],
        option_ctor=yuio.app.count_option(),
    ),
): ...
prog -qq  # quiet=2
yuio.app.store_true_option() OptionCtor[bool]

Factory for yuio.cli.StoreTrueOption.

Equivalent to using store_const_option() with const=True.

yuio.app.store_false_option() OptionCtor[bool]

Factory for yuio.cli.StoreFalseOption.

Equivalent to using store_const_option() with const=False.

type yuio.app.OptionCtor = Callable[[OptionSettings], yuio.cli.Option[T]]

CLI option constructor. Takes a single positional argument of type OptionSettings, and returns an instance of yuio.cli.Option.

class yuio.app.OptionSettings(
*,
name: str | None,
qualname: str | None,
default: Any | Missing,
parser: Parser[Any],
flags: list[str] | Positional,
required: bool,
merge: Callable[[Any, Any], Any] | None,
mutex_group: None | MutuallyExclusiveGroup,
dest: str,
help: str | Disabled,
help_group: HelpGroup | None,
usage: Collapse | bool,
default_desc: str | None,
show_if_inherited: bool,
long_flag_prefix: str,
)

Settings for creating an Option derived from field’s type and configuration.

name: str | None

Name of config field or app parameter that caused creation of this option.

qualname: str | None

Fully qualified name of config field or app parameter that caused creation of this option. Useful for reporting errors.

default: Any | Literal[_Placeholders.MISSING]

See yuio.cli.ValueOption.default.

parser: Parser[Any]

Parser associated with this option.

flags: list[str] | Literal[_Placeholders.POSITIONAL]

See yuio.cli.Option.flags.

required: bool

See yuio.cli.Option.required.

merge: Callable[[Any, Any], Any] | None

See yuio.cli.ValueOption.merge.

mutex_group: None | MutuallyExclusiveGroup

See yuio.cli.Option.mutex_group.

dest: str

See yuio.cli.Option.dest. We don’t provide any guarantees about dest‘s contents and recommend treating it as an opaque value.

help: str | Literal[_Placeholders.DISABLED]

See yuio.cli.Option.help.

help_group: HelpGroup | None

See yuio.cli.Option.help_group.

usage: Literal[_Placeholders.COLLAPSE] | bool

See yuio.cli.Option.usage.

default_desc: str | None

See yuio.cli.Option.default_desc.

show_if_inherited: bool

See yuio.cli.Option.show_if_inherited.

long_flag_prefix: str

This argument will contain prefix that was added to all flags. For apps and top level configs it will be "--", for nested configs it will include additional prefixes, for example "--nested-".

Re-imports

type yuio.app.HelpGroup

Alias of yuio.cli.HelpGroup.

type yuio.app.MutuallyExclusiveGroup

Alias of yuio.cli.MutuallyExclusiveGroup.

yuio.app.MISC_GROUP

Alias of yuio.cli.MISC_GROUP.

yuio.app.OPTS_GROUP

Alias of yuio.cli.OPTS_GROUP.

yuio.app.SUBCOMMANDS_GROUP

Alias of yuio.cli.SUBCOMMANDS_GROUP.