Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/xdg/IniFile.py: 25%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Base Class for DesktopEntry, IconTheme and IconData
3"""
5import re, os, stat, io
6from xdg.Exceptions import (ParsingError, DuplicateGroupError, NoGroupError,
7 NoKeyError, DuplicateKeyError, ValidationError,
8 debug)
9import xdg.Locale
10from xdg.util import u
12def is_ascii(s):
13 """Return True if a string consists entirely of ASCII characters."""
14 try:
15 s.encode('ascii', 'strict')
16 return True
17 except UnicodeError:
18 return False
20class IniFile:
21 defaultGroup = ''
22 fileExtension = ''
24 filename = ''
26 tainted = False
28 def __init__(self, filename=None):
29 self.content = dict()
30 if filename:
31 self.parse(filename)
33 def __cmp__(self, other):
34 return cmp(self.content, other.content)
36 def parse(self, filename, headers=None):
37 '''Parse an INI file.
39 headers -- list of headers the parser will try to select as a default header
40 '''
41 # for performance reasons
42 content = self.content
44 if not os.path.isfile(filename):
45 raise ParsingError("File not found", filename)
47 try:
48 # The content should be UTF-8, but legacy files can have other
49 # encodings, including mixed encodings in one file. We don't attempt
50 # to decode them, but we silence the errors.
51 fd = io.open(filename, 'r', encoding='utf-8', errors='replace')
52 except IOError as e:
53 if debug:
54 raise e
55 else:
56 return
58 # parse file
59 with fd:
60 for line in fd:
61 line = line.strip()
62 # empty line
63 if not line:
64 continue
65 # comment
66 elif line[0] == '#':
67 continue
68 # new group
69 elif line[0] == '[':
70 currentGroup = line.lstrip("[").rstrip("]")
71 if debug and self.hasGroup(currentGroup):
72 raise DuplicateGroupError(currentGroup, filename)
73 else:
74 content[currentGroup] = {}
75 # key
76 else:
77 try:
78 key, value = line.split("=", 1)
79 except ValueError:
80 raise ParsingError("Invalid line: " + line, filename)
82 key = key.strip() # Spaces before/after '=' should be ignored
83 try:
84 if debug and self.hasKey(key, currentGroup):
85 raise DuplicateKeyError(key, currentGroup, filename)
86 else:
87 content[currentGroup][key] = value.strip()
88 except (IndexError, UnboundLocalError):
89 raise ParsingError("Parsing error on key, group missing", filename)
91 self.filename = filename
92 self.tainted = False
94 # check header
95 if headers:
96 for header in headers:
97 if header in content:
98 self.defaultGroup = header
99 break
100 else:
101 raise ParsingError("[%s]-Header missing" % headers[0], filename)
103 # start stuff to access the keys
104 def get(self, key, group=None, locale=False, type="string", list=False, strict=False):
105 # set default group
106 if not group:
107 group = self.defaultGroup
109 # return key (with locale)
110 if (group in self.content) and (key in self.content[group]):
111 if locale:
112 value = self.content[group][self.__addLocale(key, group)]
113 else:
114 value = self.content[group][key]
115 else:
116 if strict or debug:
117 if group not in self.content:
118 raise NoGroupError(group, self.filename)
119 elif key not in self.content[group]:
120 raise NoKeyError(key, group, self.filename)
121 else:
122 value = ""
124 if list == True:
125 values = self.getList(value)
126 result = []
127 else:
128 values = [value]
130 for value in values:
131 if type == "boolean":
132 value = self.__getBoolean(value)
133 elif type == "integer":
134 try:
135 value = int(value)
136 except ValueError:
137 value = 0
138 elif type == "numeric":
139 try:
140 value = float(value)
141 except ValueError:
142 value = 0.0
143 elif type == "regex":
144 value = re.compile(value)
145 elif type == "point":
146 x, y = value.split(",")
147 value = int(x), int(y)
149 if list == True:
150 result.append(value)
151 else:
152 result = value
154 return result
155 # end stuff to access the keys
157 # start subget
158 def getList(self, string):
159 if re.search(r"(?<!\\)\;", string):
160 list = re.split(r"(?<!\\);", string)
161 elif re.search(r"(?<!\\)\|", string):
162 list = re.split(r"(?<!\\)\|", string)
163 elif re.search(r"(?<!\\),", string):
164 list = re.split(r"(?<!\\),", string)
165 else:
166 list = [string]
167 if list[-1] == "":
168 list.pop()
169 return list
171 def __getBoolean(self, boolean):
172 if boolean == 1 or boolean == "true" or boolean == "True":
173 return True
174 elif boolean == 0 or boolean == "false" or boolean == "False":
175 return False
176 return False
177 # end subget
179 def __addLocale(self, key, group=None):
180 "add locale to key according the current lc_messages"
181 # set default group
182 if not group:
183 group = self.defaultGroup
185 for lang in xdg.Locale.langs:
186 langkey = "%s[%s]" % (key, lang)
187 if langkey in self.content[group]:
188 return langkey
190 return key
192 # start validation stuff
193 def validate(self, report="All"):
194 """Validate the contents, raising :class:`~xdg.Exceptions.ValidationError`
195 if there is anything amiss.
197 report can be 'All' / 'Warnings' / 'Errors'
198 """
200 self.warnings = []
201 self.errors = []
203 # get file extension
204 self.fileExtension = os.path.splitext(self.filename)[1]
206 # overwrite this for own checkings
207 self.checkExtras()
209 # check all keys
210 for group in self.content:
211 self.checkGroup(group)
212 for key in self.content[group]:
213 self.checkKey(key, self.content[group][key], group)
214 # check if value is empty
215 if self.content[group][key] == "":
216 self.warnings.append("Value of Key '%s' is empty" % key)
218 # raise Warnings / Errors
219 msg = ""
221 if report == "All" or report == "Warnings":
222 for line in self.warnings:
223 msg += "\n- " + line
225 if report == "All" or report == "Errors":
226 for line in self.errors:
227 msg += "\n- " + line
229 if msg:
230 raise ValidationError(msg, self.filename)
232 # check if group header is valid
233 def checkGroup(self, group):
234 pass
236 # check if key is valid
237 def checkKey(self, key, value, group):
238 pass
240 # check random stuff
241 def checkValue(self, key, value, type="string", list=False):
242 if list == True:
243 values = self.getList(value)
244 else:
245 values = [value]
247 for value in values:
248 if type == "string":
249 code = self.checkString(value)
250 if type == "localestring":
251 continue
252 elif type == "boolean":
253 code = self.checkBoolean(value)
254 elif type == "numeric":
255 code = self.checkNumber(value)
256 elif type == "integer":
257 code = self.checkInteger(value)
258 elif type == "regex":
259 code = self.checkRegex(value)
260 elif type == "point":
261 code = self.checkPoint(value)
262 if code == 1:
263 self.errors.append("'%s' is not a valid %s" % (value, type))
264 elif code == 2:
265 self.warnings.append("Value of key '%s' is deprecated" % key)
267 def checkExtras(self):
268 pass
270 def checkBoolean(self, value):
271 # 1 or 0 : deprecated
272 if (value == "1" or value == "0"):
273 return 2
274 # true or false: ok
275 elif not (value == "true" or value == "false"):
276 return 1
278 def checkNumber(self, value):
279 # float() ValueError
280 try:
281 float(value)
282 except:
283 return 1
285 def checkInteger(self, value):
286 # int() ValueError
287 try:
288 int(value)
289 except:
290 return 1
292 def checkPoint(self, value):
293 if not re.match("^[0-9]+,[0-9]+$", value):
294 return 1
296 def checkString(self, value):
297 return 0 if is_ascii(value) else 1
299 def checkRegex(self, value):
300 try:
301 re.compile(value)
302 except:
303 return 1
305 # write support
306 def write(self, filename=None, trusted=False):
307 if not filename and not self.filename:
308 raise ParsingError("File not found", "")
310 if filename:
311 self.filename = filename
312 else:
313 filename = self.filename
315 if os.path.dirname(filename) and not os.path.isdir(os.path.dirname(filename)):
316 os.makedirs(os.path.dirname(filename))
318 with io.open(filename, 'w', encoding='utf-8') as fp:
320 # An executable bit signifies that the desktop file is
321 # trusted, but then the file can be executed. Add hashbang to
322 # make sure that the file is opened by something that
323 # understands desktop files.
324 if trusted:
325 fp.write(u("#!/usr/bin/env xdg-open\n"))
327 if self.defaultGroup:
328 fp.write(u("[%s]\n") % self.defaultGroup)
329 for (key, value) in self.content[self.defaultGroup].items():
330 fp.write(u("%s=%s\n") % (key, value))
331 fp.write(u("\n"))
332 for (name, group) in self.content.items():
333 if name != self.defaultGroup:
334 fp.write(u("[%s]\n") % name)
335 for (key, value) in group.items():
336 fp.write(u("%s=%s\n") % (key, value))
337 fp.write(u("\n"))
339 # Add executable bits to the file to show that it's trusted.
340 if trusted:
341 oldmode = os.stat(filename).st_mode
342 mode = oldmode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
343 os.chmod(filename, mode)
345 self.tainted = False
347 def set(self, key, value, group=None, locale=False):
348 # set default group
349 if not group:
350 group = self.defaultGroup
352 if locale == True and len(xdg.Locale.langs) > 0:
353 key = key + "[" + xdg.Locale.langs[0] + "]"
355 try:
356 self.content[group][key] = value
357 except KeyError:
358 raise NoGroupError(group, self.filename)
360 self.tainted = (value == self.get(key, group))
362 def addGroup(self, group):
363 if self.hasGroup(group):
364 if debug:
365 raise DuplicateGroupError(group, self.filename)
366 else:
367 self.content[group] = {}
368 self.tainted = True
370 def removeGroup(self, group):
371 existed = group in self.content
372 if existed:
373 del self.content[group]
374 self.tainted = True
375 else:
376 if debug:
377 raise NoGroupError(group, self.filename)
378 return existed
380 def removeKey(self, key, group=None, locales=True):
381 # set default group
382 if not group:
383 group = self.defaultGroup
385 try:
386 if locales:
387 for name in list(self.content[group]):
388 if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key:
389 del self.content[group][name]
390 value = self.content[group].pop(key)
391 self.tainted = True
392 return value
393 except KeyError as e:
394 if debug:
395 if e == group:
396 raise NoGroupError(group, self.filename)
397 else:
398 raise NoKeyError(key, group, self.filename)
399 else:
400 return ""
402 # misc
403 def groups(self):
404 return self.content.keys()
406 def hasGroup(self, group):
407 return group in self.content
409 def hasKey(self, key, group=None):
410 # set default group
411 if not group:
412 group = self.defaultGroup
414 return key in self.content[group]
416 def getFileName(self):
417 return self.filename