Skip to content

eugenetriguba/config-file

Repository files navigation

Config File

Simple manipulation of ini, json, yaml, or toml files

Python Version Version Style Build Status Codecov

About Config File

The Config File project is designed to allow you to easily manipulate your configuration files with the same simple API whether they are in INI, JSON, YAML, or TOML.

Installation

Config File is available to download through PyPI.

$ pip install config-file

Installing Extras

If you want to manipulate YAML and TOML, you'll want to download the extras as well.

$ pip install config-file[yaml, toml]

You can also use Poetry.

$ poetry install config-file -E yaml -E toml

Usage

For this overview, let's say you have the following ini file you want to manipulate.

Do note, however, that the ini format is the oddest format that ConfigFile supports in that it has no formal specification and is not type aware. When retrieving items from the file, it will return them as strings by default. Others are more type aware and do not require as much type coercion.

[section]
num_key = 5
str_key = blah
bool_key = true
list_key = [1, 2]

[second_section]
dict_key = { "another_num": 5 }

It must have a .ini extension in order for the package to recognize it and use the correct parser for it.

Setting up ConfigFile

To use the package, we import in the ConfigFile object. This object is the only thing considered to be part of the public API.

We can set it up by giving it a string or pathlib.Path as the argument. Any home tildes ~ in the string or Path are recognized and converted to the full path for us.

from config_file import ConfigFile

config = ConfigFile("~/some-project/config.ini")

Handling ConfigFile Initialization Errors

from config_file import ConfigFile, ParsingError

try:
    config = ConfigFile("~/some-file.ini")
except ParsingError:
    print("could not parse the file")
except ValueError:
    print("extension that isn't supported was used or is a directory")
except FileNotFoundError:
    print("file does not exist")

Using get()

A recurring pattern you'll see here is that all methods that need to specify something inside your configuration file will do so using a dot syntax.

Retrieving keys and sections

So to retrieve our num_key, we'd specify the heading and the key separated by a dot. All values will then be retrieved as strings.

config.get('section.num_key')
>>> '5'

While we can retrieves keys, we can also retrieve the entire section, which will be returned back to us as a dictionary.

config.get('section')
>>> {'num_key': '5', 'str_key': 'blah', 'bool_key': 'true', 'list_key': '[1, 2]'}

Furthermore, you can also index into the ConfigFile object to retrieve keys if that is preferred.

config['section']['num_key']
>>> '5'

Coercing the return types

However, some of these keys are obviously not strings natively. If we are retrieving a particular value of a key, we may want to coerce it right away without doing clunky type conversions after each time we retrieve a value. To do this, we can utilize the return_type keyword argument.

config.get('section.num_key', return_type=int)
>>> 5

Sometimes we don't have structures quite that simple though. What if we wanted all the values in section coerced? For that, we can utilize a parse_types keyword argument.

config.get('section', parse_types=True)
>>> {'num_key': 5, 'str_key': 'blah', 'bool_key': True, 'list_key': [1, 2]}

It also works for regular keys.

config.get('section.num_key', parse_types=True)
>>> 5

Handling non-existent keys

Sometimes we want to retrieve a key but are unsure of if it will exist. There are two ways we could handle that.

The first is the one we're used to seeing: catch the error.

try:
    important_value = config.get('section.i_do_not_exist')
except KeyError:
    important_value = 42

However, the get method comes with a default keyword argument that we can utilze for this purpose.

config.get('section.i_do_not_exist', default=42)
>>> 42

This can be handy if you have a default for a particular configuration value.

Using set()

We can use set() to set a existing key's value.

config.set('section.num_key', 6)

The method does not return anything, since there is nothing useful to return. If something goes wrong where it is unable to set the value, an exception will be raised instead. This is the case for most methods on ConfigFile, such as delete() or save(), where there would be no useful return value to utilize.

With set(), we can also create and set keys that don't exist yet.

config.set('new_section.new_key', 'New key value!')

Would then result in the following section being added to our original file:

[new_section]
new_key = New key value!

The exact behavior of how these new keys or sections are added are a bit dependent on the file format we're using, since every format is a little different in it's structure and in what it supports. Mostly though, ini is just the odd one.

If we try the following in ini, which does not support subsections or nested keys, we simply get a single section.

config.set("section.sub_section.sub_sub_section.key", 5)
[section.sub_section.sub_sub_section]
key = 5

Lastly, we can set values using an array notation as well. The underlying content is all manipulated as a dictionary for every file type. If we wanted to create a new section, we'd simply set it to be an empty dictionary.

config['new_section'] = {}

Which would result to be an empty section:

[new_section]

Using delete()

delete() allows us to delete entire sections or specific keys.

config.delete('section')

Would result in the entire section being removed from our configuration file. However, we can also just delete a single key.

config.delete('section.num_key')

We can also use the array notation here as well.

del config['section']['num_key']

Using has()

has() allows us to check whether a given key exists in our file. There are two ways to use has().

The first is using the dot syntax.

config.has('section.str_key')
>>> True
config.has('does_not_exist')
>>> False

This will check if our specific key or section exists. However, we can also check in general if a given key or sections exists anywhere in our file with the wild keyword argument.

config.has('str_key', wild=True)
>>> True

Using save()

For any changes we make to our configuration file, they are not written out to the filesystem until we call save(). This is to avoid unnecessary write calls after each operation until we actually need to save.

config.delete('section.list_key')
config.save()

Stringifying our ConfigFile

To retrieve the file as a string, with any changes we've made, we can use the built-in str() method on the ConfigFile. This will always show us our latest changes since it is stringify-ing our internal representation of the configuration file, not just the file we've read in.

str(config)
>>> '[section]\nnum_key = 5\nstr_key = blah\nbool_key = true\nlist_key = [1, 2]\n\n[second_section]\ndict_key = { "another_num": 5 }\n\n'

Using restore_original()

If we have a initial configuration file state, we could keep a copy of that initial file and restore back to it whenever needed using restore_original().

By default, if we created our ConfigFile object with the path of ~/some-project/config.ini, restore_original() will look for our original file at ~/some-project/config.original.ini.

config.restore_original()

However, if we have a specific path elsewhere that this original configuration file is or it is named differently than what the default expects, we can utilize the original_path keyword argument.

config.restore_original(original_path="~/some-project/original-configs/config.ini")

Format Versions Supported

Format Specification version supported
INI No official specification.
JSON RFC 7159
YAML v1.2
TOML v1.0.0-rc.1

For ini and json, Python's standard library modules are used. Regarding ini, there is no formal specification so the syntax that configparser supports is what is supported here.

License

The MIT License.