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())
Positional-only arguments become positional CLI options.
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,
Create an application.
This is a decorator that’s supposed to be used on the main method of the application. This decorator returns an
Appobject.- 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, seeApp.theme.version – program’s version, will be displayed using the
--versionflag.bug_report – settings for automated bug report generation. If present, adds the
--bug-reportflag.is_dev_mode – enables additional logging, see
App.is_dev_mode.doc_format – overrides program’s documentation format, see
App.doc_format.
- Returns:
an
Appobject 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.
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,
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
DISABLEDto 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
DISABLEDto 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‘saction: it allows overriding logic for handling CLI arguments by providing a customOptionimplementation.option_ctor should be a callable which takes a single positional argument of type
OptionSettings, and returns an instance ofyuio.cli.Option.help –
help message that will be used in CLI option description, formatted using RST or Markdown (see
App.doc_format).Pass
yuio.DISABLEDto remove this field from CLI help.help_group –
overrides group in which this field will be placed when generating CLI help message.
Pass
yuio.COLLAPSEto 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
Falseto remove this field from usage.Pass
yuio.COLLAPSEto 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,
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]_
): ...
kill_cmd.signalwill be loaded from--kill-cmd-signal.copy_cmd_2.signalwill be loaded from--kill-signal.kill_cmd_3.signalwill 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
bashsyntax, and then it will be%-formatted with a single keyword argumentprog. 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 calllogging.basicConfig()during its initialization. Disable this if you want to customize logging initialization.Disabling this option also removes the
--verboseflag 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--versionflag 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-reportflag 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 enablelogging.captureWarnings()and configure internal Yuio logging to show warnings.By default, dev mode is detected by checking if
versioncontains 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.
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,
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. passDISABLEDto 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
Appobject for a subcommand.
- lazy_subcommand( )¶
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:
Does a flag have clear and predictable behavior when its value is not specified?
For boolean flags the default behavior is obvious:
--use-gpuwill 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-threadsdo? 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.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:
High level API does not allow creating flags with optional values.
To create one, you have to make a custom implementation of
yuio.cli.Optionand set itsallow_no_argstoTrue. This will correspond to the greedy approach.Note
Positionals with defaults are treated as optional because they don’t create ambiguities.
Boolean flags allow specifying value inline, but not as a separate argument.
Yuio does not allow passing inline values to short boolean flags without adding an equals sign. For example,
-ftruewill not work, while-f=truewill.This is done to enable grouping short flags:
ls -laHshould be parsed asls -l -a -H, not asls -l=aH.On lower levels of API, Yuio allows precise control over this behavior by setting
Option.nargs,Option.allow_no_args,Option.allow_inline_arg, andOption.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 prefixno-to the first long flag of the option.- Example:
Boolean flag
--jsonimplicitly creates flag--no-json:@yuio.app.app def main( json: bool = yuio.app.field( default=False, option_ctor=yuio.app.bool_option(), ), ): ...
Boolean flag
--jsonwith 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 useparse_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()withconst=1andmerge=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()withconst=True.
- yuio.app.store_false_option() OptionCtor[bool]¶
Factory for
yuio.cli.StoreFalseOption.Equivalent to using
store_const_option()withconst=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 ofyuio.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
Optionderived from field’s type and configuration.- qualname: str | None¶
Fully qualified name of config field or app parameter that caused creation of this option. Useful for reporting errors.
- mutex_group: None | MutuallyExclusiveGroup¶
- 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.
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.