Simple manipulation of ini, json, yaml, or toml files
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.
Config File is available to download through PyPI.
$ pip install config-file
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
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.
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")
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")
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.
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'
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
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.
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]
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']
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
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()
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'
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 | 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.
The MIT License.