Config File Example#
Config files are the high-level type that are meant to manage most of the use cases of the framework. You are encouraged to extend ConfigFile to suit your needs
Basic Example#
import os
from pathlib import Path
from grave_settings.config_file import ConfigFile
from datetime import datetime
if os.path.exists('test.json'):
print(f'I dont want to overwrite your file: "test.json"')
exit()
class MyObject:
def __init__(self):
self.attribute1 = 'test'
self.attribute2 = datetime.now()
with ConfigFile(Path('test.json'), data=MyObject, formatter='json') as config:
pass # file will be saved automatically at the end of the with block
with open('test.json', 'r') as f:
print(f.read())
os.remove('test.json')
{
"__class__": "__main__.MyObject",
"attribute1": "test",
"attribute2": {
"__class__": "datetime.datetime",
"state": [
2023,
1,
24,
16,
31,
46,
143836
]
}
}
First we defined a type that will be our container. This type is MyObject
. The class is given to the ConfigFile
constructor as the named parameter data
. The reason we pass the type as opposed to an object is: if the file already existed and contained serialized data we don’t want to instantiate MyObject
just to throw it out immediately with a replacement. Also, we want the deserialization process to anticipate that existing files contain data that describes an object of the correct type to avoid attacks (this is only a very basic security measure) but more precisely to avoid errors. If the type in the file does not match the type supplied by the data
kwarg a SecurityException
is raised. This behavior exists because of the logic in get_deserialization_context()
.
When the file does not exist, MyObject
it is created by __enter__()
by calling instantiate_data()
. As long as the type doesnt have positional arguments, this should be fine.
A less basic example#
Lets make our object hierarchy more complex.
import os
from pathlib import Path
from grave_settings.config_file import ConfigFile
paths = [Path('test.json'), Path('pen1.json'), Path('pen2.json')]
for path in paths:
if path.exists():
print(f'I dont want to overwrite your file: "test.json"')
exit()
class Color:
def __init__(self, r, g, b):
self.r = r
self.g = g
self.b = b
class Pen:
def __init__(self, color: Color = None, width: int = 1):
self.color = color
self.width = width
class MyObject:
def __init__(self):
self.foreground_color = Color(255, 0, 0)
self.background_color = Color(255, 255, 255)
self.active_pen = None
self.pens: list[Pen] = []
def select_pen(self, index: int):
self.active_pen = self.pens[index]
self.foreground_color = self.active_pen.color
def add_pen(self, pen: Pen):
self.pens.append(pen)
def make_pen_config(fn: Path, pen: Pen) -> ConfigFile:
cfg = ConfigFile(fn, formatter='json')
cfg.data = pen
return cfg
with ConfigFile(Path('test.json'), data=MyObject, formatter='json') as config:
obj: MyObject = config.data
obj.add_pen(Pen(color=Color(23, 25, 25)))
obj.add_pen(Pen(color=Color(45, 45, 45), width=2))
config.add_config_dependency(make_pen_config(Path('pen1.json'), obj.pens[0]))
config.add_config_dependency(make_pen_config(Path('pen2.json'), obj.pens[1]))
obj.select_pen(0)
for path in paths:
print(f'---------- {path} -----------')
with open(path, 'r') as f:
print(f.read())
os.remove(path)
Note
This demo is meant to show how linking ConfigFile
can be done. The manner in which is is done here a questionable but it’s hard to find a good example that is simple. I just want to make it clear that linking config files should be something that has a lot more structure around it or it probably is not necessary in your program.
---------- test.json -----------
{
"__class__": "__main__.MyObject",
"active_pen": {
"__class__": "grave_settings.config_file.LogFileLink",
"rel_path": {
"__class__": "pathlib.PosixPath",
"path": "/home/ryan/.config/JetBrains/PyCharm2022.3/scratches/pen1.json",
"abs": false,
"rel_path": "pen1.json"
},
"config": {
"__class__": "grave_settings.config_file.ConfigFile",
"formatter_t": {
"__class__": "abc.ABCMeta",
"state": "grave_settings.formatters.json.JsonFormatter"
},
"data_t": {
"__class__": "builtins.type",
"state": "__main__.Pen"
}
}
},
"background_color": {
"__class__": "__main__.Color",
"b": 255,
"g": 255,
"r": 255
},
"foreground_color": {
"__class__": "__main__.Color",
"b": 25,
"g": 25,
"r": 23
},
"pens": [
{
"__class__": "grave_settings.formatter_settings.PreservedReference",
"ref": "\"active_pen\""
},
{
"__class__": "grave_settings.config_file.LogFileLink",
"rel_path": {
"__class__": "pathlib.PosixPath",
"path": "/home/ryan/.config/JetBrains/PyCharm2022.3/scratches/pen2.json",
"abs": false,
"rel_path": "pen2.json"
},
"config": {
"__class__": "grave_settings.config_file.ConfigFile",
"formatter_t": {
"__class__": "grave_settings.formatter_settings.PreservedReference",
"ref": "\"active_pen\".\"config\".\"formatter_t\""
},
"data_t": {
"__class__": "grave_settings.formatter_settings.PreservedReference",
"ref": "\"active_pen\".\"config\".\"data_t\""
}
}
}
]
}
---------- pen1.json -----------
{
"__class__": "__main__.Pen",
"color": {
"__class__": "__main__.Color",
"b": 25,
"g": 25,
"r": 23
},
"width": 1
}
---------- pen2.json -----------
{
"__class__": "__main__.Pen",
"color": {
"__class__": "__main__.Color",
"b": 45,
"g": 45,
"r": 45
},
"width": 2
}
Lets point out something important about add_config_dependency()
, as of right now nothing is shared between the config files. This includes semantics and references. This means that “is” relationships are not shared between config files. This can be done, but I’m not sure if I need it enough to work out the kinks. It should be doable within the ConfigFile
. It may be that this behavior would not be desirable since the file being referenced may change, and that could be just as “unexpected” to naive code then not preserving “is” relationships.
Lets preserve the “is” relationship#
This is not using the currently unimplemented ideas above, just an example of using the interface to handle a special case.
class MyObject:
def __init__(self):
self.foreground_color = Color(255, 0, 0)
self.background_color = Color(255, 255, 255)
self.active_pen = None
self.pens: list[Pen] = []
def select_pen(self, index: int):
self.active_pen = self.pens[index]
self.foreground_color = self.active_pen.color
def add_pen(self, pen: Pen):
self.pens.append(pen)
def to_dict(self, *args, **kwargs):
base = Serializable.to_dict(self, *args, **kwargs)
base['active_pen'] = self.pens.index(self.active_pen)
return base
def from_dict(self, state_object: dict, *args, **kwargs):
active_pen_idx = state_object['active_pen']
state_object['active_pen'] = state_object['pens'][active_pen_idx]
Serializable.from_dict(self, state_object, *args, **kwargs)
This new structure for MyObject
would not be compatible with the old one but it does do it’s job. If we wanted the active_pen
and foreground_color
to maintain their synchronicity we could simply call select_pen
after repurposing from_dict()