1
+ __author__ = "Jiri Novotny"
2
+ __version__ = "1.0.0"
3
+
4
+ class KeyValues (dict ):
5
+ """
6
+ Class for manipulation with Valve KeyValue (KV) files (VDF format). Parses the KV file to object with dict interface.
7
+ Allows to write objects with dict interface to KV files.
8
+ """
9
+
10
+ __re = __import__ ('re' )
11
+ __sys = __import__ ('sys' )
12
+ __OrderedDict = __import__ ('collections' ).OrderedDict
13
+ __regexs = {
14
+ "key" : __re .compile (r"""(['"])(?P<key>((?!\1).)*)\1(?!.)""" , __re .I ),
15
+ "key_value" : __re .compile (r"""(['"])(?P<key>((?!\1).)*)\1(\s+|)['"](?P<value>((?!\1).)*)\1""" , __re .I )
16
+ }
17
+
18
+ def __init__ (self , mapper = None , filename = None , encoding = "utf-8" , mapper_type = __OrderedDict , key_modifier = None , key_sorter = None ):
19
+ """
20
+ :param mapper: initialize with own dict-like mapper
21
+ :param filename: filename of KV file, which will be parsed to dict structure. Mapper param must not be specified when using this param!
22
+ :param encoding: KV file encoding. Default: 'utf-8'
23
+ :param mapper_type: which mapper will be used for storing KV. It must have the dict interface, i.e. allow to do the 'mapper[key] = value action'.
24
+ default: 'collections.OrderedDict'
25
+ For example you can use the 'dict' type.
26
+ :param key_modifier: function for modifying the keys, e.g. the function 'string.lower' will make all the keys lower
27
+ :param key_sorter: function for sorting the keys when dumping/writing/str, e.g. using the function 'sorted' will show KV keys in alphabetical order
28
+ """
29
+
30
+ self .__sys .setrecursionlimit (100000 )
31
+ self .mapper_type = type (mapper ) if mapper else mapper_type
32
+ self .key_modifier = key_modifier
33
+ self .key_sorter = key_sorter
34
+ self .encoding = encoding
35
+
36
+ if not mapper and not filename :
37
+ self .mapper = mapper_type ()
38
+ return
39
+
40
+ if mapper :
41
+ self .mapper = mapper
42
+ return
43
+
44
+ if type (filename ) == str :
45
+ self .parse (filename )
46
+ else :
47
+ raise Exception ("'filename' argument must be string!" )
48
+
49
+ def __setitem__ (self , key , item ):
50
+ self .mapper [key ] = item
51
+
52
+ def __getitem__ (self , key ):
53
+ return self .mapper [key ]
54
+
55
+ def __repr__ (self ):
56
+ #return repr(self.mapper)
57
+ return self .dump (self .mapper )
58
+
59
+ def __len__ (self ):
60
+ return len (self .mapper )
61
+
62
+ def __delitem__ (self , key ):
63
+ del self .mapper [key ]
64
+
65
+ def clear (self ):
66
+ return self .mapper .clear ()
67
+
68
+ def copy (self ):
69
+ """
70
+ :return: mapper of KeyValues
71
+ """
72
+ return self .mapper .copy ()
73
+
74
+ def has_key (self , k ):
75
+ return self .mapper .has_key (k )
76
+
77
+ def pop (self , k , d = None ):
78
+ return self .mapper .pop (k , d )
79
+
80
+ def update (self , * args , ** kwargs ):
81
+ return self .mapper .update (* args , ** kwargs )
82
+
83
+ def keys (self ):
84
+ return self .mapper .keys ()
85
+
86
+ def values (self ):
87
+ return self .mapper .values ()
88
+
89
+ def items (self ):
90
+ return self .mapper .items ()
91
+
92
+ def pop (self , * args ):
93
+ return self .mapper .pop (* args )
94
+
95
+ def __cmp__ (self , dict ):
96
+ return cmp (self .mapper , dict )
97
+
98
+ def __contains__ (self , item ):
99
+ return item in self .mapper
100
+
101
+ def __iter__ (self ):
102
+ return iter (self .mapper )
103
+
104
+ def __unicode__ (self ):
105
+ return unicode (repr (self .mapper ))
106
+
107
+ def __str__ (self ):
108
+ return self .dump ()
109
+
110
+ def __key_modifier (self , key , key_modifier ):
111
+ """
112
+ Modifies the key string using the 'key_modifier' function.
113
+
114
+ :param key:
115
+ :param key_modifier:
116
+ :return:
117
+ """
118
+
119
+ key_modifier = key_modifier or self .key_modifier
120
+
121
+ if key_modifier :
122
+ return key_modifier (key )
123
+ else :
124
+ return key
125
+
126
+ def __parse (self , lines , mapper_type , i = 0 , key_modifier = None ):
127
+ """
128
+ Recursively maps the KeyValues from list of file lines.
129
+
130
+ :param lines:
131
+ :param mapper_type:
132
+ :param i:
133
+ :param key_modifier:
134
+ :return:
135
+ """
136
+
137
+ key = False
138
+ _mapper = mapper_type ()
139
+
140
+ try :
141
+ while i < len (lines ):
142
+ if lines [i ].startswith ("{" ):
143
+ if not key :
144
+ raise Exception ("'{{' found without key at line {}" .format (i + 1 ))
145
+ _mapper [key ], i = self .__parse (lines , i = i + 1 , mapper_type = mapper_type , key_modifier = key_modifier )
146
+ continue
147
+ elif lines [i ].startswith ("}" ):
148
+ return _mapper , i + 1
149
+ elif self .__re .match (self .__regexs ["key" ], lines [i ]):
150
+ key = self .__key_modifier (self .__re .search (self .__regexs ["key" ], lines [i ]).group ("key" ), key_modifier )
151
+ i += 1
152
+ continue
153
+ elif self .__re .match (self .__regexs ["key_value" ], lines [i ]):
154
+ groups = self .__re .search (self .__regexs ["key_value" ], lines [i ])
155
+ _mapper [self .__key_modifier (groups .group ("key" ), key_modifier )] = groups .group ("value" )
156
+ i += 1
157
+ elif self .__re .match (self .__regexs ["key_value" ], lines [i ] + lines [i + 1 ]):
158
+ groups = self .__re .search (self .__regexs ["key_value" ], lines [i ] + " " + lines [i + 1 ])
159
+ _mapper [self .__key_modifier (groups .group ("key" ), key_modifier )] = groups .group ("value" )
160
+ i += 1
161
+ else :
162
+ i += 1
163
+ except IndexError :
164
+ pass
165
+
166
+ return _mapper
167
+
168
+ def parse (self , filename , encoding = "utf-8" , mapper_type = __OrderedDict , key_modifier = None ):
169
+ """
170
+ Parses the KV file so this instance can be accessed by dict interface.
171
+
172
+ :param filename: name of KV file
173
+ :param encoding: KV file encoding. Default: 'utf-8'
174
+ :param mapper_type: which mapper will be used for storing KV. It must have the dict interface, i.e. allow to do the 'mapper[key] = value action'.
175
+ default: 'collections.OrderedDict'
176
+ For example you can use the 'dict' type.
177
+ This will override the instance's 'mapper_type' if specified during instantiation.
178
+ :param key_modifier: function for modifying the keys, e.g. the function 'string.lower' will make all the keys lower.
179
+ This will override the instance's 'key_modifier' if specified during instantiation.
180
+ """
181
+
182
+ with open (filename , mode = "r" , encoding = encoding ) as f :
183
+ self .mapper = self .__parse ([line .strip () for line in f .readlines ()],
184
+ mapper_type = mapper_type or self .mapper_type ,
185
+ key_modifier = key_modifier or self .key_modifier )
186
+
187
+ def __tab (self , string , level , quotes = False ):
188
+ if quotes :
189
+ return '{}"{}"' .format (level * "\t " , string )
190
+ else :
191
+ return '{}{}' .format (level * "\t " , string )
192
+
193
+ def __dump (self , mapper , key_sorter = None , level = 0 ):
194
+ string = ""
195
+
196
+ if key_sorter :
197
+ keys = key_sorter (mapper .keys ())
198
+ else :
199
+ keys = mapper .keys ()
200
+
201
+ for key in keys :
202
+ string += self .__tab (key , level , quotes = True )
203
+ if type (mapper [key ]) == str :
204
+ string += '\t "{}"\n ' .format (mapper [key ])
205
+ else :
206
+ string += "\n " + self .__tab ("{\n " , level )
207
+ string += self .__dump (mapper [key ], key_sorter = key_sorter , level = level + 1 )
208
+ string += self .__tab ("}\n " , level )
209
+
210
+ return string
211
+
212
+ def dump (self , mapper = None , key_sorter = None ):
213
+ """
214
+ Dumps the KeyValues mapper to string.
215
+
216
+ :param mapper: you can dump your own object with dict interface
217
+ :param key_sorter: function for sorting the keys when dumping/writing/str, e.g. using the function 'sorted' will show KV in alphabetical order.
218
+ This will override the instance's 'key_sorter' if specified during instantiation.
219
+ :return: string
220
+ """
221
+
222
+ return self .__dump (mapper = mapper or self .mapper , key_sorter = key_sorter or self .key_sorter )
223
+
224
+ def write (self , filename , encoding = "utf-8" , mapper = None , key_sorter = None ):
225
+ """
226
+ Writes the KeyValues to file.
227
+
228
+ :param filename: output KV file name
229
+ :param encoding: output KV file encoding. Default: 'utf-8'
230
+ :param mapper: you can write your own object with dict interface
231
+ :param key_sorter: key_sorter: function for sorting the keys when dumping/writing/str, e.g. using the function 'sorted' will show KV in alphabetical order.
232
+ This will override the instance's 'key_sorter' if specified during instantiation.
233
+ """
234
+
235
+ with open (filename , mode = "w" , encoding = encoding ) as f :
236
+ f .write (self .dump (mapper = mapper or self .mapper , key_sorter = key_sorter or self .key_sorter ))
0 commit comments