Quick Start¶
The steps below describe how to create, modify, save, merge, subclass Configs and much more. Similarly to many other parts of this documentation Quick Start section is generated out of actual unit tests that cover the functionality described below.
Create config¶
Config objects are initialized using arbitrary number of mappings and keyword arguments.
Any config
object can be populated by assigning values directly or by merging another config into it.
Values can assigned to the keys that do not exist yet.
merge
method is almost identical to the Config()
constructor. Actually, Config()
just uses merge
inside itself.
The only difference is that the constructor, obviously, creates new object, while merge
is merging values into an existing one.
from ilexconf import Config, from_json, from_env, to_json
# Empty config
config = Config()
assert dict(config) == {}
# Create config from json and merge it into our initial config
# Let settings_json_file_path = "settings.json" where inside the file we have
# { "database": { "connection": { "host": "localhost", "port": 5432 } } }
config.merge(from_json(settings_json_file_path))
assert dict(config) == {
"database": {"connection": {"host": "localhost", "port": 5432}}
}
# Merge dict into config
config.merge({"database": {"connection": {"host": "test.local"}}})
assert dict(config) == {
"database": {"connection": {"host": "test.local", "port": 5432}}
}
# Merge environment variables into config
config.merge(from_env(prefix="AWS_", separator="__").lower(inplace=True))
assert dict(config) == {
"database": {"connection": {"host": "test.local", "port": 5432}},
"default_region": "us-east-1",
}
# Merge keyword arguments
config.set("my__keyword__argument", True, key_sep="__")
assert dict(config) == {
"database": {"connection": {"host": "test.local", "port": 5432}},
"default_region": "us-east-1",
"my": {"keyword": {"argument": True}},
}
# Clear values, just like with dict
config.clear()
assert dict(config) == {}
# Or, better yet, do this all in one step, since Config() constructor
# accepts any number of mapping objects and keyword arguments as
# initialization parameters. However, order of parameters matters.
# Last mappings are merged on top of others. And keywords override even that.
config = Config(
from_json(settings_json_file_path),
{"database": {"connection": {"host": "test.local"}}},
database__connection__port=4000,
)
assert dict(config) == {
"database": {"connection": {"host": "test.local", "port": 4000}}
}
When we initialize config all the values are merged. Arguments are merged in order they are provided. Every next argument is merged on top of the previous mapping values. And keyword arguments override even that. For more details read about merging below.
Read configs from different sources¶
Files like .json, .yaml, .toml, .ini, .env, .py as well as environment variables can all be read & loaded using a set of from_ functions.
cfg1 = from_json(settings_json_file_path)
assert dict(cfg1) == {
"database": {"connection": {"host": "localhost", "port": 5432}}
}
Access values¶
You can access any key in the hierarchical structure using classical Python dict notation, dotted keys, attributes, or any combination of this methods.
# Classic way
assert config["database"]["connection"]["host"] == "test.local"
# Dotted key notation
assert config["database.connection.host"] == "test.local"
# Via attributes
assert config.database.connection.host == "test.local"
# Any combination of the above
assert config["database"].connection.host == "test.local"
assert config.database["connection.host"] == "test.local"
assert config.database["connection"].host == "test.local"
assert config.database.connection["host"] == "test.local"
Upsert values (update or insert)¶
Similarly, you can set values of any key (even if it doesn’t exist in the Config) using all of the ways above.
Notice, contrary to what you would expect from the Python dictionaries, setting nested keys that do not exist is **allowed**.
# Change value that already exists in the dictionary
# just like you would do with simple dict
config["database"]["connection"]["port"] = 8080
assert config["database"]["connection"]["port"] == 8080
# Create new value using 'dotted notation'. Notice that
# 'user' field did not exist before.
config["database.connection.user"] = "root"
assert config["database.connection.user"] == "root"
# Create new value using. 'password' field did not exist
# before we assigned a value to it and was created automatically.
config.database.connection.password = "secret stuff"
assert config.database.connection.password == "secret stuff"
Merging values¶
If you just assign a value to any key, you override any previous value of that key. In order to merge assigned value with an existing one, use merge method.
# Config correctly merges nested values. Notice how it overrides
# the value of the 'password' key in the nested 'connection' config
# from 'secret stuff' to 'different secret'
config.database.connection.merge({"password": "different secret"})
assert config.database.connection.password == "different secret"
merge respects the contents of each value. For example, merging two dictionaries with the same key would not override that key completely. Instead, it will recursively look into each key and try to merge the contents. Take this example:
merged = Config(
{"a1": {"c1": 1, "c2": 2, "c3": 3}}, {"a1": {"c3": "other"}}
)
# Instead of overriding the value of the "a1" key completely, `merge` method
# will recursively look inside and merge nested values.
assert dict(merged) == {"a1": {"c1": 1, "c2": 2, "c3": "other"}}
Convert to dict¶
For any purposes you might find fit you can convert entire structure of the Config object into dictionary, which will be essentially returned to you as a deep copy of the object.
assert dict(config) == {
"database": {
"connection": {
"host": "test.local",
"port": 8080,
"user": "root",
"password": "different secret",
}
}
}
Save config to file¶
You can serialize the file as json
, toml
, ini
or other types any time using a set of to_
functions.
# Temporary path
p = tmp_path / "settings.json"
# Save config
to_json(config, p)
# Verify written file is correct
assert dict(from_json(p)) == {
"database": {
"connection": {
"host": "test.local",
"port": 8080,
"user": "root",
"password": "different secret",
}
}
}
Subclass and add your own logic¶
Subclassing Config
class is very convenient for implementation of your own config classes with custom logic.
Consider this example:
class MyConfig(Config):
def __init__(self, do_stuff=False):
# Initialize your custom config using json settings file
super().__init__(from_json(settings_json_file_path))
# Add some custom value depending on some logic
if do_stuff:
# Here, we create new nested key that did not exist
# before and assign a value to it.
self.my.custom.key = "Yes, do stuff"
# Merge one more mapping on top
self.merge({"Horizon": "Up"})
Here is what will get generated when we instantiate this custom MyConfig
object.
# Now you can use your custom defined Config. Given the `setting.json` file that
# contains { "database": { "connection": { "host": "localhost", "port": 5432 } } }
# MyConfig will have the following values:
config = MyConfig(do_stuff=True)
assert dict(config) == {
"database": {
"connection": {
"host": "localhost",
"port": 5432,
},
},
"Horizon": "Up",
"my": {"custom": {"key": "Yes, do stuff"}},
}