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

279 statements  

1""" 

2Base Class for DesktopEntry, IconTheme and IconData 

3""" 

4 

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 

11 

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 

19 

20class IniFile: 

21 defaultGroup = '' 

22 fileExtension = '' 

23 

24 filename = '' 

25 

26 tainted = False 

27 

28 def __init__(self, filename=None): 

29 self.content = dict() 

30 if filename: 

31 self.parse(filename) 

32 

33 def __cmp__(self, other): 

34 return cmp(self.content, other.content) 

35 

36 def parse(self, filename, headers=None): 

37 '''Parse an INI file. 

38  

39 headers -- list of headers the parser will try to select as a default header 

40 ''' 

41 # for performance reasons 

42 content = self.content 

43 

44 if not os.path.isfile(filename): 

45 raise ParsingError("File not found", filename) 

46 

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 

57 

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) 

81 

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) 

90 

91 self.filename = filename 

92 self.tainted = False 

93 

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) 

102 

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 

108 

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 = "" 

123 

124 if list == True: 

125 values = self.getList(value) 

126 result = [] 

127 else: 

128 values = [value] 

129 

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) 

148 

149 if list == True: 

150 result.append(value) 

151 else: 

152 result = value 

153 

154 return result 

155 # end stuff to access the keys 

156 

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 

170 

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 

178 

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 

184 

185 for lang in xdg.Locale.langs: 

186 langkey = "%s[%s]" % (key, lang) 

187 if langkey in self.content[group]: 

188 return langkey 

189 

190 return key 

191 

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. 

196  

197 report can be 'All' / 'Warnings' / 'Errors' 

198 """ 

199 

200 self.warnings = [] 

201 self.errors = [] 

202 

203 # get file extension 

204 self.fileExtension = os.path.splitext(self.filename)[1] 

205 

206 # overwrite this for own checkings 

207 self.checkExtras() 

208 

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) 

217 

218 # raise Warnings / Errors 

219 msg = "" 

220 

221 if report == "All" or report == "Warnings": 

222 for line in self.warnings: 

223 msg += "\n- " + line 

224 

225 if report == "All" or report == "Errors": 

226 for line in self.errors: 

227 msg += "\n- " + line 

228 

229 if msg: 

230 raise ValidationError(msg, self.filename) 

231 

232 # check if group header is valid 

233 def checkGroup(self, group): 

234 pass 

235 

236 # check if key is valid 

237 def checkKey(self, key, value, group): 

238 pass 

239 

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] 

246 

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) 

266 

267 def checkExtras(self): 

268 pass 

269 

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 

277 

278 def checkNumber(self, value): 

279 # float() ValueError 

280 try: 

281 float(value) 

282 except: 

283 return 1 

284 

285 def checkInteger(self, value): 

286 # int() ValueError 

287 try: 

288 int(value) 

289 except: 

290 return 1 

291 

292 def checkPoint(self, value): 

293 if not re.match("^[0-9]+,[0-9]+$", value): 

294 return 1 

295 

296 def checkString(self, value): 

297 return 0 if is_ascii(value) else 1 

298 

299 def checkRegex(self, value): 

300 try: 

301 re.compile(value) 

302 except: 

303 return 1 

304 

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", "") 

309 

310 if filename: 

311 self.filename = filename 

312 else: 

313 filename = self.filename 

314 

315 if os.path.dirname(filename) and not os.path.isdir(os.path.dirname(filename)): 

316 os.makedirs(os.path.dirname(filename)) 

317 

318 with io.open(filename, 'w', encoding='utf-8') as fp: 

319 

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")) 

326 

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")) 

338 

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) 

344 

345 self.tainted = False 

346 

347 def set(self, key, value, group=None, locale=False): 

348 # set default group 

349 if not group: 

350 group = self.defaultGroup 

351 

352 if locale == True and len(xdg.Locale.langs) > 0: 

353 key = key + "[" + xdg.Locale.langs[0] + "]" 

354 

355 try: 

356 self.content[group][key] = value 

357 except KeyError: 

358 raise NoGroupError(group, self.filename) 

359 

360 self.tainted = (value == self.get(key, group)) 

361 

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 

369 

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 

379 

380 def removeKey(self, key, group=None, locales=True): 

381 # set default group 

382 if not group: 

383 group = self.defaultGroup 

384 

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

401 

402 # misc 

403 def groups(self): 

404 return self.content.keys() 

405 

406 def hasGroup(self, group): 

407 return group in self.content 

408 

409 def hasKey(self, key, group=None): 

410 # set default group 

411 if not group: 

412 group = self.defaultGroup 

413 

414 return key in self.content[group] 

415 

416 def getFileName(self): 

417 return self.filename