Usage¶
First install the package with:
pip install configupdater
or add it to a project managed with uv:
uv add configupdater
Now we can simply do:
from configupdater import ConfigUpdater
updater = ConfigUpdater()
updater.read("setup.cfg")
which would read the file setup.cfg that is found in many projects.
To change the value of an existing key we can simply do:
updater["metadata"]["author"].value = "Alan Turing"
At any point we can print the current state of the configuration file with:
print(updater)
To update the read-in file, with the encoding it was read with, just call
updater.update_file() or updater.write(fp) with a file object fp to write the
changed configuration file to another destination.
Before actually writing, ConfigUpdater will automatically check that the updated
configuration file is still valid by parsing it with the help of ConfigParser.
Many of ConfigParser’s methods still exist, and it’s best to look them up in the API reference. Let’s look at some examples.
Adding and removing options¶
Let’s say we have the following configuration in a string:
cfg = """
[metadata]
author = Ada Lovelace
summary = The Analytical Engine
"""
We can add a license option, i.e. a key/value pair, in the same way we would do with ConfigParser:
updater = ConfigUpdater()
updater.read_string(cfg)
updater["metadata"]["license"] = "MIT"
A simple print(updater) will show you that the new option was appended to the end:
[metadata]
author = Ada Lovelace
summary = The Analytical Engine
license = MIT
Since the license is really important to us, let’s say we want to add it before the summary
and even add a short comment before it:
updater = ConfigUpdater()
updater.read_string(cfg)
(
updater["metadata"]["summary"]
.add_before.comment("Ada would have loved MIT")
.option("license", "MIT")
)
which would result in:
[metadata]
author = Ada Lovelace
# Ada would have loved MIT
license = MIT
summary = The Analytical Engine
Using add_after would give the same result and looks like:
updater = ConfigUpdater()
updater.read_string(cfg)
(
updater["metadata"]["author"]
.add_after.comment("Ada would have loved MIT")
.option("license", "MIT")
)
Let’s say we want to rename summary to the more common description:
updater = ConfigUpdater()
updater.read_string(cfg)
updater["metadata"]["summary"].key = "description"
If we wanted no summary at all, we could just do del updater["metadata"]["summary"].
Option keys and case sensitivity¶
Like ConfigParser, ConfigUpdater passes option keys through
optionxform() before comparing them.
The default is str.lower(), so updater["metadata"]["AUTHOR"] finds the
author option, while the file keeps the original spelling, which is also
available as raw_key.
To make keys case-sensitive, set optionxform before reading the file:
updater = ConfigUpdater()
updater.optionxform = str
updater.read_string(cfg)
Warning
As in ConfigParser, a custom optionxform must be idempotent, i.e.
optionxform(optionxform(key)) == optionxform(key), because the keys that
ConfigUpdater returns, e.g. when iterating over a section, are already
transformed and are transformed again when you use them for a lookup.
For example, with lambda key: "prefix_" + key,
[section[key] for key in section] raises a KeyError: iterating over
a section with a license option yields prefix_license, which is then
looked up as prefix_prefix_license.
Adding and removing sections¶
Adding and removing sections works just like adding and removing options, but on a higher level.
Sticking to our Ada Lovelace example, let’s say we want to add a section options just
before metadata with a comment and two blank lines to separate it from metadata:
updater = ConfigUpdater()
updater.read_string(cfg)
(
updater["metadata"]
.add_before.comment("Some specific project options")
.section("options")
.space(2)
)
As expected, this results in:
# Some specific project options
[options]
[metadata]
author = Ada Lovelace
summary = The Analytical Engine
We could now fill the new section with options like we learnt before. If we wanted to rename
an existing section, we could do this with the help of the name attribute:
updater["metadata"].name = "MetaData"
Sometimes it might be useful to inject a new section not in a programmatic way but more declaratively. Let’s assume we have thus defined our new section in a multi-line string:
sphinx_sect_str = """
[build_sphinx]
source_dir = docs
build_dir = docs/_build
"""
With the help of two ConfigUpdater objects we can easily inject this section into our example:
sphinx = ConfigUpdater()
sphinx.read_string(sphinx_sect_str)
sphinx_sect = sphinx["build_sphinx"]
updater = ConfigUpdater()
updater.read_string(cfg)
(updater["metadata"].add_after.space().section(sphinx_sect.detach()))
The detach() method removes the build_sphinx
section from the first object, so that it can be added to the second one. This results in:
[metadata]
author = Ada Lovelace
summary = The Analytical Engine
[build_sphinx]
source_dir = docs
build_dir = docs/_build
Alternatively, if you want to preserve build_sphinx in both
ConfigUpdater objects (i.e., prevent it from being
removed from the first while still adding a copy to the second), you can also
rely on the stdlib’s copy.deepcopy() function instead of
detach():
from copy import deepcopy
sphinx = ConfigUpdater()
sphinx.read_string(sphinx_sect_str)
sphinx_sect = sphinx["build_sphinx"]
updater = ConfigUpdater()
updater.read_string(cfg)
(updater["metadata"].add_after.space().section(deepcopy(sphinx_sect)))
This technique can be used for all objects inside ConfigUpdater: sections, options, comments and blank spaces.
Shallow copies are discouraged in the context of ConfigUpdater because each configuration block keeps a reference to its container to allow easy document editing. When making edits (such as adding or changing options and comments) based on a shallow copy, the results can be unreliable and unexpected.
Keeping comments immediately before a section¶
add_before inserts directly before the section block. If you have added
comments immediately before that section using the builder, they end up before
the newly inserted block, separated from the section. To keep those comments next to the original section,
use add_before_skip_comments instead:
updater = ConfigUpdater()
updater.read_string("[first]\n")
(
updater["first"]
.add_after.comment("Settings for the target section")
.section("target")
)
(updater["target"].add_before_skip_comments.section("middle").space())
This results in:
[first]
[middle]
# Settings for the target section
[target]
Only consecutive comment blocks at the document level are skipped. Blank spaces stop the search, and comments inside a previous section are not moved. This does not change how the parser associates comments with sections.
For more examples of how the API of ConfigUpdater works, it’s best to take a look at the unit tests and read the API reference.