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.