Getting Started

This tutorial introduces LayeredConfigTree, a configuration data structure where values can be set at multiple priority layers. By default, reading a value returns the one from the highest-priority layer that has it defined.

Creating a Tree

At its simplest, a tree can be created from a dictionary:

from layered_config_tree import LayeredConfigTree

print(LayeredConfigTree({"greeting": "hello"}))
greeting:
    base: hello

Note in the example above that by default a single “base” layer is used. You can also specify layers in order from lowest to highest priority. For more on layers and priority, see Layers and Priority.

Note

Layers only pertain to values, not to sub-trees.

Adding Data

Use update() to add data at a specific layer. Data is provided as a (possibly nested) dictionary:

tree = LayeredConfigTree(layers=["base", "override"])
tree.update(
    {"name": "some_service", "database": {"host": "localhost", "port": 5432}},
    layer="base",
    source="defaults.yaml",
)
tree.update(
    {"database": {"host": "prod-server"}},
    layer="override",
    source="environment",
)

The source parameter is optional metadata that records where a value came from, which is useful for debugging.

In addition to calling the update() method, you can update an existing key using direct assignment (=). This sets the value at the highest-priority layer available for that key and records the source as None:

t = LayeredConfigTree({"x": 1}, layers=["base", "override"])
t.x = 99
print(repr(t))
x:
    override: 99
        source: None
    base: 1
        source: initial data

If a value has already been set for that key at the highest priority level, a DuplicatedConfigurationError is raised:

try:
    t.x = 88
except Exception as e:
    print(type(e).__name__)
DuplicatedConfigurationError

New keys cannot be created via assignment — you must use update() for that:

try:
    t.new_key = 5
except Exception as e:
    print(type(e).__name__)
ConfigurationKeyError

In additional to providing data directly, you can initialize or update a tree from YAML strings or a path to a YAML file.

print(LayeredConfigTree("server:\n  host: localhost\n  port: 8080\n"))
server:
    host:
        base: localhost
    port:
        base: 8080

Reading Values

There are four ways to read from a LayeredConfigTree, each with different behavior.

Note

All four access methods can be chained together and/or mixed and matched as desired.

Dot and bracket notation

Both dot access (tree.key) and bracket notation (tree["key"]) return the child of the highest-priority layer:

print(tree.name)
print(tree["database"])
some_service
host:
    override: prod-server
port:
    base: 5432

Notice that host returns "prod-server" (from the override layer), not "localhost" (from the base layer). The port value was only set at the base layer, so that value is returned.

A ConfigurationKeyError will be raised of the requested key does not exist at any layer.

Note

Keys that look like Python dunder attributes (starting and ending with double underscores __) can only be accessed via bracket notation to avoid conflicting with Python’s internal attribute machinery:

answer = LayeredConfigTree({"__special__": 42})

# Bracket notation works
print(answer["__special__"])

# Dot notation raises an error
try:
    answer.__special__
except Exception as e:
    print(type(e).__name__)
42
ImproperAccessError

Note

Keys that are not valid Python variable names (e.g. those containing spaces or special characters) can also only be accessed via bracket notation:

weird = LayeredConfigTree({"space key": "foo", "dash-key": "bar"})

# Bracket notation works
print(weird["space key"])
print(weird["dash-key"])
foo
bar

Dot notation will not work for these keys. weird.space key is a SyntaxError (Python cannot parse it at all), and weird.dash-key is interpreted as weird.dash - key, which raises a ConfigurationKeyError because "dash" is not a key in the tree.

try:
    print(weird.dash-key)
except Exception as e:
    print(type(e).__name__)
ConfigurationKeyError

get() method access

get() works like dict.get() and returns a default value (None by default) when the key is missing instead of raising an error. It also accepts a list of keys for nested lookups and supports a layer parameter to read from a specific layer:

print(tree.get("name"))                                 # same as dot access
print(tree.get("missing"))                              # returns None
print(tree.get("missing", default_value="fallback"))    # custom default
print(tree.get(["database", "host"]))                   # nested lookup
print(tree.database.get("host", layer="base"))          # specific layer
some_service
None
fallback
prod-server
localhost

get_tree() method access

get_tree() guarantees the result is a sub-tree. Note that it does not support a layer argument or return a default value like get().

print(tree.get_tree("database"))  # OK — returns a sub-tree
host:
    override: prod-server
port:
    base: 5432

If the value at the key path is a leaf, it raises ConfigurationError:

try:
    tree.get_tree("name")
except Exception as e:
    print(type(e).__name__)
ConfigurationError

Printing a Tree

Printing a tree (str) shows each value at its highest-priority layer:

print(tree)
name:
    base: some_service
database:
    host:
        override: prod-server
    port:
        base: 5432

Meanwhile, the repr shows all layers along with source information:

print(repr(tree))
name:
    base: some_service
        source: defaults.yaml
database:
    host:
        override: prod-server
            source: environment
        base: localhost
            source: defaults.yaml
    port:
        base: 5432
            source: defaults.yaml

Converting to a Dictionary

Use to_dict() to extract a plain dictionary of highest priority information.

print(tree.to_dict())
{'name': 'some_service', 'database': {'host': 'prod-server', 'port': 5432}}

Note that all layer and source metadata is discarded.