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"}},
    }