Commands to manage application settings
Typical applications built with Typer
are essentially stateless. That is, to control their behavior, you need to
provide all of the configuration for the app through the use of positional arguments, options, and environment
variables.
For a complex application with many commands, this can be frustrating and slow. You find yourself passing the same parameters over and over.
Thus, typerdrive
provides a settings
subcommand to help with this.
Overview
The typerdrive
package provides functionality to store, reuse, and update application settings through a set of
subcommands. These subcommands are bound to your app under the settings
subcommand. These subcommands manipulate the
your app's settings and allow your other commands to access the settings values via the @attach_settings
decorator.
Let's take a look at how we can use this powerful feature set.
Usage
Let's start by looking at a code example:
In this example, the app provides a Pydantic model that describes all of the
settings values that the app needs. Then, the app calls the add_settings_subcommand()
to add the settings
feature to
the CLI. That's all you need to utilize the settings
feature in your app. Now, you can access and manage your settings
through the various settings
subcommands.
In the report
command, you can see how the settings values may be accessed within one of the app's other commands. The
@attach_settings
decorator adds the settings object to the app's typer.Context
. Then, the settings can be accessed
by providing a parameter to the command that matches the SettingsModel
type. The argument that will get the settings
object can be named anything you like!
Settings model type agreement
The type of the pydantic model passed to @attach_settings()
MUST match the type used for the settings
parameter of the command function. If the types do not match, a Typer
exception will be raised saying that Typer
doesn't know how to handle the argument.
Great, now let's try a few commands in this app to see how the settings commands work.
First, we will just show the config
$ python examples/settings/commands.py settings show
╭─ Current settings ──────────────────────────────────────────────────────────────────────────╮
│ │
│ is-humanoid -> True │
│ alignment -> neutral │
│ │
│ Configuration is invalid: │
│ name -> Field required │
│ planet -> Field required │
│ │
╰─────────────────────────────────────────────────────────────────────────────────────────────╯
As you can see, our settings initially just matches the defaults provided in the settings model. The fields that still need to be defined are clearly identified and the settings are shown to be invalid.
Next, let's make the settings valid by setting the missing values with bind
:
╭─ Current settings ──────────────────────────────────────────────────────────────────────────╮
│ │
│ name -> jawa │
│ planet -> tatooine │
│ is-humanoid -> True │
│ alignment -> neutral │
│ │
╰─ saved to /home/dusktreader/.local/share/commands.py/settings.json ─────────────────────────╯
Now, the settings are valid. You can also see that the settings were saved to disk for your app to use in future commands.
Let's make an adjustment to the settings using the update
command:
$ python examples/settings/commands.py settings update --name=hutt --no-is-humanoid
╭─ Current settings ──────────────────────────────────────────────────────────────────────────╮
│ │
│ name -> hutt │
│ planet -> tatooine │
│ is-humanoid -> False │
│ alignment -> neutral │
│ │
╰─ saved to /home/dusktreader/.local/share/commands.py/settings.json ─────────────────────────╯
Notice that the update
command only changed the values specified and left the others alone.
Now that we're happy with our settings, lets run our report
command to try out using these app settings:
$ python examples/settings/commands.py report
Look at this neutral hutt from tatooine slithering by.
Great! Our app is able to use the settings in any command!
Finally, let's clear out the settings with reset
:
╭─ Current settings ──────────────────────────────────────────────────────────────────────────╮
│ │
│ is-humanoid -> True │
│ alignment -> neutral │
│ │
│ Configuration is invalid: │
│ name -> Field required │
│ planet -> Field required │
│ │
╰─ saved to /home/dusktreader/.local/share/commands.py/settings.json ─────────────────────────╯
Now, all the settings are returned to their initial values. Those that have no default values are now invalid.
Details
Let's take a closer look at details of each settings
subcommand.
bind
The bind
command is used to set all your app settings at once. It is very similar to the update
command with a few
key differences. First, the bind
command will not allow you to have an invalid configuration when it is done. It will
require each settings value without a default to be explicitly set. After you have provided the values through command
options, the final configuration will be validated before it is saved.
Like the other settings
subcommands that modify the settings, bind
will write a settings file to disk when it is
finished. The settings file is stored in ~/.local/share/<your-app-name>/settings.json
. If the parent directories for
this file don't exist, they will be created.
Not supported on Windows
Currently, the typerdrive
settings
commands are only configured to work on Linux and MacOS. I have plans to add
support for Windows as well eventually, but at the moment typerdrive
is dependent on settings being stored below
~/.local/share
Each settings value from the settings model you provide is mapped to a CLI option for the bind
subcommand. If the
value has a default in the model, then the option will use the same default. Boolean values use the normal convention
from Typer with --flag
or --no-flag
controlling the value of the boolean.
The help text from our example above for the bind
subcommand looks like this:
$ python examples/settings/commands.py settings bind --help
Usage: commands.py settings bind [OPTIONS]
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ * --name TEXT [default: None] [required] │
│ * --planet TEXT [default: None] [required] │
│ --is-humanoid --no-is-humanoid [default: is-humanoid] │
│ --alignment TEXT [default: neutral] │
│ --help Show this message and exit. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
update
The update
command is used to update a subset of the available settings values. It works very similarly to the bind
command, however, the update
command will allow your configuration to be invalid when it is finished. This might be
useful if you want to establish some values in your settings now but need to look something up before you are finished
configuring the app.
Like the other subcommands that modify settings, update
will save all changes to disk.
Each settings value from the settings model is mapped to an optional CLI option for the update
subcommand. If the
settings value is a boolean, it will use the --flag
/ --no-flag
format. All other commands will default to None
if
they are not passed and the update
command will ignore them.
The help text from our example above for the update
subcommand looks like this:
$ python examples/settings/commands.py settings update --help
Usage: commands.py settings update [OPTIONS]
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --name TEXT [default: None] │
│ --planet TEXT [default: None] │
│ --is-humanoid --no-is-humanoid [default: is-humanoid] │
│ --alignment TEXT [default: None] │
│ --help Show this message and exit. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Notice how now all the options have a default.
unset
The unset
command is used to return a settings value to its initial state. If the value has a default, it will be set
to that value. If it does not have a default, it will simply be removed. Like the update
subcommand, unset
allows
the settings to be in invalid state.
Each settings value from the settings model is mapped to a CLI option that takes no value
. If you supply the option,
then the corresponding setting value will be unset.
The help text from our example above for the unset
subcommand looks like this:
$ python examples/settings/commands.py settings unset --help
Usage: commands.py settings unset [OPTIONS]
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --name │
│ --planet │
│ --is-humanoid │
│ --alignment │
│ --help Show this message and exit. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
show
The show
command just shows the current value of the settings. That's it!
reset
The reset
command returns all settings values to their initial state. It allows the settings to be in an invalid
state when it is finished. It will also show the new settings values when it is done. The reset
subcommand takes no
arguments.
The get_settings()
functions
In order for typerdrive
to provide the settings through an argument to the command function, we have to tap into a bit
of Python and Typer's "mystical energy field". If you want to use something more direct, you can access the settings
object using the get_settings()
function to extract it from the typer.Context
instead. Rewriting the report()
command to use the get_settings()
function would look like this:
@cli.command()
@attach_settings(SettingsModel)
def report(ctx: typer.Context):
cfg = get_settings(ctx, SettingsModel)
print(
unwrap(
f"""
Look at this {cfg.alignment} {cfg.name} from {cfg.planet}
{'walking' if cfg.is_humanoid else 'slithering'} by.
"""
)
)
The type_hint
argument to get_settings()
Because the model is bound to the settings commands dynamically, the get_settings()
function needs a type hint
to cast it to the appropriate model type. This type_hint
argument must match with the settings model that was
attached or an exception will be raised.
The add_.*()
functions
The typerdrive.settings.commands
module has several add_.*()
functions. These work by adding a subcommand to the
CLI app that is passed in. In general, you only need to use the add_settings_subcommand()
in your app. However, if you
want to customize where the settings subcommands appear, you may call the other add_.*()
functions directly
add_bind()
This method adds the bind
subcommand to the provided CLI app. It uses the
build_command()
function to dynamically create a command
and then adds it to the cli
argument.
add_update()
This method adds the update
subcommand to the provided CLI app. It uses the
build_command()
function to dynamically create a command
and then adds it to the cli
argument.
add_unset()
This method adds the unset
subcommand to the provided CLI app. It uses the
build_command()
function to dynamically create a command
and then adds it to the cli
argument.
add_show()
This method adds the show
subcommand to the provided CLI app. It uses the
build_command()
function to dynamically create a command
and then adds it to the cli
argument.
add_reset()
This method adds the reset
subcommand to the provided CLI app. It uses the
build_command()
function to dynamically create a command
and then adds it to the cli
argument.
add_settings_subcommand()
This method does three things:
- Creates a new Typer app
- Adds all the settings subcommands to the new app
- Adds the new app as a subcommand of the Typer CLI that you provide
The result is that all the subcommands are available under one settings
subcommand.