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