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.
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, seeyuio.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()
withenv
andflags
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()
withflags
set toPOSITIONAL
.
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.
- 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 calllogging.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()