pythonconfigconfiguration-files

How to properly read and write .cfg files through python


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.


Solution

  • 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.