App

This module expands on yuio.config to build CLI apps.

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 = positional(),
...     #: help message for `--flag`.
...     flag: int = 0
... ):
...     """this command does a thing."""
...     yuio.io.info("flag=%r, arg=%r", flag, arg)

>>> # 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()) 
flag=10 arg='foobar!'

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

yuio.app.app(command: Callable[[...], None] | None = None, /, *, prog: str | None = None, usage: str | None = None, help: str | None = None, description: str | None = None, epilog: str | None = None)[source]

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.

See App for description of function’s parameters.

class yuio.app.App(command: Callable[[...], None], /, *, prog: str | None = None, usage: str | None = None, help: str | None = None, description: str | None = None, epilog: str | None = None)[source]

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: Sequence[str] | None = None) NoReturn[source]

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

If no args are given, parse sys.argv.

Configuring flags and options

Main function parameters, much like Config fields, can be configured. Their names and type hints are used to derive default values, which can be overridden by the field() function. They can also contain other configs, essentially allowing you to nest CLI flags.

yuio.app.field(default: ~typing.Any = _Placeholders.MISSING, *, parser: ~yuio.parse.Parser[~typing.Any] | None = None, help: str | ~typing.Literal[<disabled>] | None = None, env: str | ~typing.Literal[<disabled>] | None = None, flags: str | ~typing.List[str] | ~typing.Literal[<positional>] | ~typing.Literal[<disabled>] | None = None, completer: ~yuio.complete.Completer | None = None) Any[source]

Field descriptor, used for additional configuration of fields.

Parameters:
  • default – default value for config field, used if field is missing from config.

  • parser – parser that will be used to parse env vars, configs and CLI arguments.

  • help

    Help message that will be used in CLI argument description.

    Pass DISABLED to remove this field from CLI help.

    By default, help message is inferred from comments right above the field definition (comments must start with #:).

    Help messages are formatted using Markdown (see yuio.md).

  • env

    Name of environment variable that will be used for this field.

    Pass DISABLED to disable loading this field form environment variable.

    Pass an empty string to disable prefixing nested config variables.

  • flags

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

    This setting is used with yuio.app to configure CLI arguments parsers.

    Pass DISABLED to disable loading this field form CLI arguments.

    Pass POSITIONAL to make this argument positional (only in apps, see yuio.app).

    Pass an empty string to disable prefixing nested config flags.

  • completer

    completer that will be used for autocompletion in CLI.

    This setting is used with yuio.app to configure CLI arguments parsers.

yuio.DISABLED: Literal[<disabled>] = _Placeholders.DISABLED

Indicates that some functionality is disabled.

yuio.MISSING: Literal[<missing>] = _Placeholders.MISSING

Indicates that some value is missing.

yuio.POSITIONAL: Literal[<positional>] = _Placeholders.POSITIONAL

Used with field() to enable positional arguments.

There are also a few helpers that are specific to the yuio.app module:

yuio.app.inline(help: str | ~typing.Literal[<disabled>] | None = None) Any[source]

A shortcut for inlining nested configs.

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

yuio.app.positional(default: ~typing.Any = _Placeholders.MISSING, *, parser: ~yuio.parse.Parser[~typing.Any] | None = None, help: str | ~typing.Literal[<disabled>] | None = None, env: str | ~typing.Literal[<disabled>] | None = None, completer: ~yuio.complete.Completer | None = None) Any[source]

A shortcut for adding a positional argument.

Equivalent to calling field() with flags set to POSITIONAL.

Flag names are derived from field names. Use the field() function to override them:

@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(None, flags=['-p', '--pid'])
):
    ...

In nested configs, flags are prefixed with name of a field that contains the nested config:

class KillCmdConfig(Config):
    # Will be loaded from `--signal`.
    signal: int

    # Will be loaded from `-p` or `--pid`.
    pid: int = field(flags=['-p', '--pid'])

@app
def main(
    # `kill_cmd.signal` will be loaded from `--kill-cmd-signal`.
    kill_cmd: KillCmdConfig,

    # `copy_cmd_2.signal` will be loaded from `--kill-signal`.
    kill_cmd_2: KillCmdConfig = field(flags='--kill'),

    # `kill_cmd_3.signal` will be loaded from `--signal`.
    kill_cmd_3: KillCmdConfig = field(flags=''),
):
    ...

Note

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

Help messages for the flags are parsed from line comments right above the field definition (comments must start with #:). This works for both functions and config classes. The field() function allows overriding them. Help messages are all formatted using the Markdown (see yuio.md).

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`.
    verbose: bool = True,

    # Will create a flag with `nargs=*`: `--inputs path1 path2 ...`
    inputs: list[Path],
):

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[source]
prog: str | None

Program or subcommand display name.

By default, inferred from sys.argv and subcommand names.

See prog.

usage: str | None

Program or subcommand synapsis.

This string will be colorized according 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, generated from CLI flags by argparse.

See usage.

description: str | None

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 levels.

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 description.

See description.

help: str | None

Short help message that is shown when listing subcommands.

See help.

epilog: str | None

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

Text format is identical to the one for description.

See epilog.

allow_abbrev: bool

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

Disabled by default.

See allow_abbrev.

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.

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.

Creating sub-commands

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

class yuio.app.App[source]
subcommand(cb_or_name: str | Callable[[...], None] | None = None, /, *, name: str | None = None, aliases: List[str] | None = None, prog: str | None = None, usage: str | None = None, help: str | None = None, description: str | None = None, epilog: str | None = None)[source]

Register a subcommand for the given app.

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

For example:

@app
def main(): ...

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

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

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.

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.
        ...
class yuio.app.CommandInfo(name: str, subcommand: CommandInfo | None, _config: Any)[source]

Data about the invoked command.

name: str

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__'.

subcommand: CommandInfo | None

Subcommand of this command, if one was given.

You can call the subcommand on your own by using _command_info as a callable. However, you must return False from your function, this will prevent Yuio from calling subcommand for a second time:

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

    # Ensure that Yuio doesn't call a subcommand again.
    # This interface is similar to one of __exit__ functions,
    # where you return `True` to suppress an exception.
    return False
class yuio.app.CommandInfo(name: str, subcommand: CommandInfo | None, _config: Any)[source]

Data about the invoked command.

__call__() Literal[False][source]

Execute this command.

This function always returns False, so you can call a subcommand and exit from a parent command in a single line:

@app
def main(_command_info: CommandInfo):
    if _command_info.subcommand is not None:
        # Manually call a subcommand and return `False`:
        return _command_info()