Command line interfaces¶
Exploring Yuio apps and CLI interfaces.
Creating and using apps¶
Yuio provides a decorator that turns functions into CLI applications:
yuio.app.app(). Function parameters become application’s CLI arguments;
you can configure them the same way as you configure config fields:
import yuio.app
@yuio.app.app
def main(
#: Who are we greeting?
greeting: str = yuio.app.field(
default="world",
flags=["-g", "--greeting"],
metavar="<name>",
)
):
print(f"Hello, {greeting}!")
Positional-only arguments become positional CLI options, while keyword arguments become CLI flags. **kwargs are not supported.
You can run the app with method run():
if __name__ == "__main__":
main.run()
run() sets up Yuio and logging, parses CLI arguments, and invokes
the main function.
You can invoke the original main function using wrapped():
main.wrapped(greeting="sunshine")
Tip
Set App.is_dev_mode to True
to see helpful warnings from Yuio.
Positional arguments¶
By default, all app arguments become flags. You can use positional-only arguments to create positional CLI options:
@yuio.app.app
def main(
#: Who are we greeting?
greeting: str,
/, # [1]_
):
print(f"Hello, {greeting}!")
This syntax separates positional-only arguments from normal arguments. See PEP 570 for details.
Mutually exclusive arguments¶
Sometimes you need to ensure that only one of several arguments can be passed.
For this, create a MutuallyExclusiveGroup and pass it to all
such arguments:
GROUP = yuio.app.MutuallyExclusiveGroup()
@yuio.app.app
def main(
release: str,
alpha: bool = yuio.app.field(default=False, mutex_group=GROUP),
beta: bool = yuio.app.field(default=False, mutex_group=GROUP),
rc: bool = yuio.app.field(default=False, mutex_group=GROUP),
):
...
Using configs in CLI¶
You can use Config to group CLI arguments. This can help with
encapsulation and reduce code duplication:
import pathlib
import yuio.app
import yuio.config
class ExecutorConfig(yuio.config.Config):
#: Number of threads to use.
threads: int = 4
#: Enable gpu usage.
use_gpu: bool = True
@yuio.app.app
def main(
#: File to process.
input: pathlib.Path,
/,
#: Executor options.
executor_config: ExecutorConfig,
):
...
By default, Yuio will prefix all flags in the nested config with flag of config’s
field. That is, ExecutorConfig.threads will be loaded from
--executor-config-threads.
You can override this prefix by passing flag to yuio.app.field(),
or disable prefixing by using yuio.app.inline():
@yuio.app.app
def main(
#: File to process.
input: pathlib.Path,
/,
#: Executor options.
executor_config: ExecutorConfig = yuio.app.inline(),
):
...
Argument groups¶
Yuio automatically groups CLI options when you use
nested Configs:
class ExecutorConfig(yuio.config.Config):
#: Number of threads to use.
threads: int = 4
#: Enable gpu usage.
use_gpu: bool = True
@yuio.app.app
def main(
#: Executor options. [1]_
#:
#: These options control algorithm execution,
#: resource usage, and acceleration. [2]_
executor_config: ExecutorConfig,
):
...
First paragraph becomes group’s title.
All consequent paragraphs are shown in group’s help section.
CLI options are not grouped when no documentation comment is available.
Additionally, you can disable grouping by setting help_group to None:
@yuio.app.app
def main(
executor_config: ExecutorConfig = yuio.app.inline(
help_group=None # [1]_
),
):
...
This moves config’s fields to the main group.
You can also assign groups for individual fields by passing
HelpGroup to yuio.app.field():
import pathlib
import yuio.app
CONFIG_GROUP = yuio.app.HelpGroup("Config")
@yuio.app.app
def main(
#: Override default path to config.
config: pathlib.Path | None = yuio.app.field(
default=None,
help_group=CONFIG_GROUP,
),
#: Print output in machine readable format.
machine_readable: bool = yuio.app.field(
default=False,
help_group=yuio.app.MISC_GROUP,
),
):
...
if __name__ == "__main__":
main.run()
Subcommands¶
We can easily build apps with multiple subcommands
by using App.subcommand:
@yuio.app.app
def main():
pass
@main.subcommand
def backup(file: pathlib.Path, /):
bak = file.parent / (file.name + ".bak")
shutil.copy(file, bak)
@main.subcommand
def restore(file: pathlib.Path, /):
bak = file.parent / (file.name + ".bak")
shutil.rmtree(file, ignore_errors=True)
shutil.move(str(bak), file)
When you run app backup ..., main() will be called first, then backup().
This lets you run code that’s needed for all sub-commands, such as loading configs.
See details in Controlling how sub-commands are invoked.
App settings¶
We can further customize our app and subcommands using the decorator’s arguments or directly setting app properties.
For example, let’s add aliases for subcommands, and also an epilog section to our app’s help:
@yuio.app.app
def main(
#: Overwrite old backup file if it exists.
force: bool = False,
):
"""
A simple tool for creating ``.bak`` files.
"""
main.epilog = """
Usage examples
--------------
- This will copy ``./prod.sqlite3`` to ``./prod.sqlite3.bak``:
.. code-block:: bash
python main.py backup ./prod.sqlite3
- This will move ``./prod.sqlite3.bak`` to ``./prod.sqlite3``:
.. code-block:: bash
python main.py restore ./prod.sqlite3
"""
@main.subcommand(aliases=["b"])
def backup(file: pathlib.Path, /):
"""
Move ``file`` to ``file.bak``.
"""
bak = file.parent / (file.name + ".bak")
shutil.copy(file, bak)
@main.subcommand(aliases=["r"])
def restore(file: pathlib.Path, /):
"""
Move ``file.bak`` to ``file``.
"""
bak = file.parent / (file.name + ".bak")
shutil.rmtree(file, ignore_errors=True)
shutil.move(str(bak), file)
This will result in the following help message:
Autocompletion¶
Yuio can generate autocompletion for Bash, Fish, Zsh, and PowerShell.
It installs completions automatically, without any need to pipe shell scripts
into specific files. Just run your app with --completions flag:
$ ./app --completions
CLI options with custom behavior¶
If default behavior doesn’t meet your needs, you can provide your own option
implementation by passing option_ctor to yuio.app.field(). This will let you
manually configure option’s parsing and help formatting:
@yuio.app.app
def main(
quiet: int = yuio.app.field(
default=0,
flags=["-q", "--quiet"],
option_ctor=yuio.app.count_option(),
)
):
...
See also
See Creating custom CLI options, yuio.app.OptionCtor,
and yuio.cli for API reference.