How to properly read and write .cfg files through python

Question:

I need help with reading and writing to .cfg files in Python. The .cfg files that I want to read and write have a specific format which cannot be changed.

An example .cfg file:

[META]
title = "Xxxx xxxx"
creator = "xxx"
artist = "xxxx"
artist = "xxx xxx"
title = "xxx xxx (xxxxxx)"
length = "4:59"

Some .cfg files can also have extremely long and formats (that I find weird). This is just a snippet of a very long .cfg file:

[EASY]
bpm = 100
offset = 0
spawn = [63, 111, 161, 201, 285, 339, 342, 347, 380, 388, 422, 449, 470, 507, 511, 531, 551, 555, 583, 591, 634, 638, 642, 701, 783]
half_spawn = [0, 1, 8, 16]
initial_data = {
    "type": 1,
    "animation": "xxxx.png",
    "looping": false,
    "fx": "",
    "background": [
        "xxxx.png",
        {
            "static": false
        }
    ],
    "voice_bank": {

    }
}

I’ve used libraries like libconf and configparser but none of these understand the file format and they report multiple errors.

The most luck I’ve had while trying to read this type of file is using something like this:

lines = open('./test.cfg').read().splitlines()

But even that results in multiple formatting issues and it’s difficult to work with. I’ve seen other posts about this but they don’t have quite the same format.

I’m not very experienced with Python, any help is appreciated.

Asked By: OsRaMoSaO

||

Answers:

One possible solution could be to use the python-libconf library, which is a pure-Python

this is an example of use:

import libconf
with io.open('file.cfg', encoding='utf-8') as f:
    cfg = libconf.load(f)
Answered By: YowoSK

You can write a small state machine to support the "complex" multi-line values, and the standard library’s ast.literal_eval() and json.loads() to turn the values to Python data.

This will fail for invocations such as Vector3(0, 0, 0) which are valid in Godot files, but you can pretty easily add code to e.g. skip those, or use e.g. the pure-eval library to deal with them.

Assuming the data_fp variable is an open file (I used an io.StringIO() for testing),

import ast
import io
import json
import sys

def parse_godot_value(value: str):
    # The Godot complex value seems to be pretty much just JSON.
    return json.loads(value)


def parse_godotesque_ini(fp):
    current_section = None
    complex_key = None
    complex_value = None
    for line in fp:
        line = line.rstrip()
        if not line:
            continue
        if line.startswith("["):
            current_section = line.strip("[]")
            continue
        if complex_value is None:  # Not busy parsing a complex value
            key, _, value = line.partition("=")
            key = key.strip()
            value = value.strip()
            if not (key and value):
                continue
            if value == "{":
                complex_key = key
                complex_value = ["{"]  # Start of a complex value
            else:
                yield (current_section, key, parse_godot_value(value))
        else:  # Busy parsing a complex value
            complex_value.append(line)
            if line == "}":  # End of a complex value
                yield (current_section, complex_key, parse_godot_value("n".join(complex_value)))
                complex_key = None
                complex_value = None


for section, key, value in parse_godotesque_ini(data_fp):
    print(section, key, value)

prints out

META title Xxxx xxxx
META creator xxx
META artist xxxx
META artist xxx xxx
META title xxx xxx (xxxxxx)
META length 4:59
EASY bpm 100
EASY offset 0
EASY spawn [63, 111, 161, 201, 285, 339, 342, 347, 380, 388, 422, 449, 470, 507, 511, 531, 551, 555, 583, 591, 634, 638, 642, 701, 783]
EASY half_spawn [0, 1, 8, 16]
EASY initial_data {'type': 1, 'animation': 'xxxx.png', 'looping': False, 'fx': '', 'background': ['xxxx.png', {'static': False}], 'voice_bank': {}}

for the data you pasted.

Good luck!


EDIT:

For emitting a similar format,

def write_godotesque_ini(fp, data_dict):
    for section, items in data_dict.items():
        fp.write(f"[{section}]n")
        for key, value in items.items():
            indent = 4 if isinstance(value, dict) else None
            fp.write(f"{key} = {json.dumps(value, indent=indent)}n")
        fp.write("n")


new_data = {
    "META": {
        "title": "Xxxx xxxx",
        "creator": "xxx",
    },
    "EASY": {
        "half_spawn": [0, 1, 8, 16],
        "initial_data": {
            "type": 1,
            "animation": "xxxx.png",
        },
    },
}

write_godotesque_ini(sys.stdout, new_data)

writes

[META]
title = "Xxxx xxxx"
creator = "xxx"

[EASY]
half_spawn = [0, 1, 8, 16]
initial_data = {
    "type": 1,
    "animation": "xxxx.png"
}

to standard output.

Answered By: AKX
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.