配置文件的格式有很多:ini,json,xml,yaml,toml等。
ini功能过于简单;
json手写不够方便;
xml复杂且难于编辑和阅读;
yaml规则太多而且坑也不少(比如不支持多行字符串);
因此toml横空出世,语法优雅,易于阅读,灵活且严谨,它可能是目前配置文件这个场景下最合适的解决方案。
但它也有不够完美的地方,笔者在使用过程中就发现,yaml中支持的继承功能(虽然用起来很恶心),在toml中是不支持的。
比如如下两个toml:
Default:
[openconfig-optical-amplifier]
name.len = [2, 20]
gain_tilt.range = [-1, 1]
“a:b/c/d”.len = [1, 20]
Ver1:
[openconfig-optical-amplifier]
name.len = [2, 30]
如果想在Ver1中复用Default,目前似乎没有很好的办法。
所以笔者动手做了一些改造,使用一个 ‘>’ 标记将多个toml块分开,命名并在其后括号中注明继承自哪个父块,分别去读这些块,再跟继承的块合并,格式如下:
>Default:
[openconfig-optical-amplifier]
name.len = [2, 20]
gain_tilt.range = [-1, 1]
"a:b/c/d".len = [1, 20]
>Ver1(Default):
[openconfig-optical-amplifier]
name.len = [2, 30]
>Ver2(Default, Ver1):
a = 1
最终输出结果:
{
"openconfig-optical-amplifier": {
"name": {"len": [2, 20]},
"gain_tilt":{"range": [-1, 1]},
"a:b/c/d": {"len" = [1, 20]}
},
"Ver1": {
"name": {"len": [2, 30]},
"gain_tilt":{"range": [-1, 1]},
"a:b/c/d": {"len" = [1, 20]}
},
"Ver2": {
"a": 1,
"name": {"len": [2, 30]},
"gain_tilt":{"range": [-1, 1]},
"a:b/c/d": {"len" = [1, 20]}
}
}
代码实现:
"""
@Author: Sanmao.Wu
@File: inheritableToml.py
@Time: 2022/2/24 22:12
@FileDesc:
"""
from copy import deepcopy
import toml
import pprint
def merge(source, destination):
"""
Function: Merge two dict by recursion way.
run me with nosetests --with-doctest file.py
>>> a = { 'first' : { 'all_rows' : { 'pass' : 'dog', 'number' : '1' } } }
>>> b = { 'first' : { 'all_rows' : { 'fail' : 'cat', 'number' : '5' } } }
>>> merge(b, a) == { 'first' : { 'all_rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } }
True
"""
for key, value in source.items():
if isinstance(value, dict):
# get node or create one
node = destination.setdefault(key, {})
merge(value, node)
else:
destination[key] = value
return destination
class InheritableToml:
def __init__(self):
self.text = ''
self.dict = {}
def __parse_title(self, title):
"""
Parse title like:
e.g:
blockA(blockB, blockC)
or
blockA
"""
if '(' in title:
class_name, others = title.split('(', maxsplit=1)
parents = [item.strip() for item in others.rstrip(')').split(',')]
else:
class_name = title
parents = []
return class_name, parents
def iter_block(self):
"""
A toml block is guided by '>' symbol, following which is block name.
e.g: >blockA:
a.b = 1
>blockB:
c = 2
"""
block, class_name, parents = [], '', []
for line in self.text.split('\n'):
print(line)
if not line:
continue
line = line.strip()
if line.startswith('>'):
if block:
toml_block = '\n'.join(block)
yield class_name, parents, toml_block
block = []
title = line.strip('>:').strip()
class_name, parents = self.__parse_title(title)
else:
block.append(line)
toml_block = '\n'.join(block)
yield class_name, parents, toml_block
def gather_blocks(self):
"""
Gather all blocks into one dict, thus we can reference any block without thinking about appearance order
"""
blocks = {}
for class_name, parents, toml_block in self.iter_block():
blocks[class_name] = {}
blocks[class_name]['parents'] = parents
blocks[class_name]['dict'] = toml.loads(toml_block)
return blocks
def inherit(self):
"""
Process inherit relationship.
e.g: >blockB(blockA):
blockB will inherit from blockA and then update its own paths.
"""
blocks = self.gather_blocks()
for class_name, block in blocks.items():
parents = block['parents']
self.dict[class_name] = {}
for parent in parents:
parent_dict = blocks[parent].get('dict')
if parent_dict is None:
raise NameError(f"No such parent block: '{parent}'")
self.dict[class_name] = merge(parent_dict, self.dict[class_name])
self.dict[class_name] = merge(block['dict'], self.dict[class_name])
def load(self, io):
self.text = io.read()
self.inherit()
return self.dict
def loads(self, text):
self.text = text
self.inherit()
return self.dict
if __name__ == '__main__':
s = '''
>Default:
[openconfig-optical-amplifier]
name.len = [2, 20]
gain_tilt.range = [-1, 1]
"a:b/c/d".len = [1, 20]
>Ver1(Default):
[openconfig-optical-amplifier]
name.len = [2, 30]
>Ver2(Default, Ver1):
a = 1
'''
pprint.pprint(InheritableToml().loads(s))