Getting Started

This page briefly explains how to install and use Typed Settings. It gives you an overview of the most important features without going into detail. At the end you’ll find some hints how to proceed from here.

Installation

Install typed-settings into your virtualenv:

$ python -m pip install typed-settings
...
Successfully installed ... typed-settings-x.y.z

Basic Settings Definition and Loading

Settings are defined as attrs classes. You can either use the decorators provided by attrs or the settings() decorator that comes with Typed Settings. This decorator is an alias to attr.define(), but it additionally defines an auto-converter for option values:

>>> import typed_settings as ts
>>>
>>> @ts.settings
... class Settings:
...     username: str = ""
...     password: str = ts.secret(default="")
...
>>> Settings("monty", "S3cr3t!")
Settings(username='monty', password=***)

As you can see, the string "433" has automatically been converted into an int when we created the instance.

Settings should (but are not required to) define defaults for all options. If an option has no default and no config value can be found for it, attrs will raise an error.

In real life, you don’t manually instantiate your settings. Instead, you call the function load():

>>> ts.load(Settings, appname="myapp")
Settings(username='', password=***)

The first argument of that function is your settings class and an instance of that class is returned by it. The second argument is your appname. That value is being used to determine the config file section and prefix for environment variables. You can override both, though.

Settings from Environment Variables

The easiest way to override an option’s default value is to set an environment variable. Typed Settings will automatically look for environment variables matching APPNAME_OPTION_NAME (in all caps):

>>> import os
>>>
>>> # Temporarily set some environment variables:
>>> monkeypatch = getfixture("monkeypatch")
>>> monkeypatch.setattr(os, "environ", {"MYAPP_USERNAME": "monty", "MYAPP_PASSWORD": "S3cr3t!"})
>>>
>>> ts.load(Settings, appname="myapp")
Settings(username='monty', password=***)
>>>
>>> monkeypatch.undo()

You can optionally change the prefix or disable loading environment variables completely. The guide Settings from Environment Variables shows you how.

Settings from Config Files

To persist settings and avoid exporting environment variables again and again, you may want to use config files. Typed Settings uses TOML files for this (Why?) and looks for the appname section by default:

>>> from pathlib import Path
>>>
>>> # Create a temporary config file:
>>> tmp_path: Path = getfixture("tmp_path")
>>> settings_file = tmp_path.joinpath("settings.toml")
>>> settings_file.write_text("""
... [myapp]
... username = "monty"
... password = "S3cr3t!"
... """)
49
>>> ts.load(Settings, appname="myapp", config_files=[settings_file])
Settings(username='monty', password=***)

You can also load settings from multiple files. Subsequent files override the settings of their predecessors.

Dynamically Finding Config Files

Sometimes, tools do not know the location of their config file in advance. Take black, for example, which searches for pyproject.toml from the current working dir upwards until it reaches the project or file system root.

You can do the same with Typed Settings:

>>> monkeypatch.chdir(tmp_path)
>>>
>>> ts.load(Settings, appname="myapp", config_files=[ts.find("settings.toml")])
Settings(username='monty', password=***)
>>>
>>> monkeypatch.undo()

find() returns a single path, so you can combine its result with a static list of files as shown in the section above.

Dynamically Specifying Config Files

You may not always know the paths of config files in advance, or you want to allow your users to specify additional ones. Typed Settings looks for an environment variable named APPNAME_SETTINGS (you can change or disable this). The variable can contain one ore more paths separated by a colon (:):

>>> monkeypatch.setenv("MYAPP_SETTINGS", str(settings_file))
>>>
>>> ts.load(Settings, appname="myapp")
Settings(username='monty', password=***)
>>>
>>> monkeypatch.undo()

Config files specified via an environment variable are loaded after statically defined ones.

By default, no error will be raised if a config file does not exist. However, you can mark files as mandatory if you want an error instead. You can read more about this in the guide Working with Config Files.

Command Line Options with Click

Some tools (like Pytest or .pypirc) allow you store settings in a config file and override them on-the-fly via command line options.

Typed Settings can integrate with click and automatically create command line options for your settings. When you run your app, settings will first be loaded from config files and environment variables. The loaded values then serve as defaults for the correspoinding click options.

Your CLI function receives all options as the single instance of your settings class:

>>> import click
>>> import click.testing
>>>
>>> @ts.settings
... class Settings:
...     username: str = ts.option(help="Your username")
...     password: str = ts.secret(default="", help="Your password")
>>>
>>> @click.command()
... @ts.click_options(Settings, ts.default_loaders("myapp"))
... def cli(settings):
...     print(settings)
...
>>> # The "CliRunner" allows us to run our CLI right here in the Python shell:
>>> runner = click.testing.CliRunner()
>>> print(runner.invoke(cli, ["--help"]).output)
Usage: cli [OPTIONS]

Options:
  --username TEXT  Your username  [required]
  --password TEXT  Your password  [default: ***]
  --help           Show this message and exit.

>>> print(runner.invoke(cli, ["--username=guido", "--password=1234"]).output)
Settings(username='guido', password=***)

Frozen Settings and Updating Them

Settings are mutable by default but can optionally be made immutable:

>>> @ts.settings(frozen=True)
... class FrozenSettings:
...     x: int
...     y: list
...
>>> settings = FrozenSettings(3, [])
>>> settings.x = 4
Traceback (most recent call last):
  ...
attr.exceptions.FrozenInstanceError

However, this does not extend to mutable option values:

>>> settings.y.append(4)
>>> print(settings)
FrozenSettings(x=3, y=[4])

Immutable settings can be desirable because they prevent you or your users from (accidentally) changing them while the app is running.

But especially when you are testing your app, you may still want to modify your settings. You can create an updated copy of your settings via attr.evolve()

>>> from attr import evolve
>>> updated = evolve(settings, x=7)
>>> print(settings)
FrozenSettings(x=3, y=[4])
>>> print(updated)
FrozenSettings(x=7, y=[4])
>>> settings is updated
False

How to Proceed

If you have read this far, you should now have a basic understanding of how Typed Settings works and what it is capable of (No, I still don’t have a clue!).

Depending on what kind of learner you are, you can now either

  • continue reading the Guides that explain all of Typed Settings’ features in-depth or

  • take a lookt at the Examples that demonstrate how Typed Settings can be used or how to achieve different kinds of goals.